diff --git a/package.json b/package.json index 982d862c..9af1ea55 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "long": "^5.3.2", "mime-types": "^2.1.27", "mongodb": "6.9.0", + "ms": "^2.1.3", "mustache": "^4.2.0", "nestjs-pino": "^4.1.0", "node-cache": "5.1.2", diff --git a/src/apps/app_sdk/AppConsumer.ts b/src/apps/app_sdk/AppConsumer.ts index 05695de1..fdaf5587 100644 --- a/src/apps/app_sdk/AppConsumer.ts +++ b/src/apps/app_sdk/AppConsumer.ts @@ -7,6 +7,12 @@ import { DelayedError } from 'bullmq'; import { PinoLogger } from 'nestjs-pino'; import { Logger } from 'pino'; +export interface JobDataTimeout { + timeout?: { + job?: number; + }; +} + /** * Base class for app consumers that provides common functionality * like mutex locking, logging setup, and error handling. @@ -27,6 +33,14 @@ export abstract class AppConsumer extends BaseWorkerHost { }); } + protected signal(job) { + if (job.data?.timeout?.job != null) { + return AbortSignal.timeout(job.data?.timeout?.job); + } + // Aka never abort + return new AbortController().signal; + } + /** * Executes a job with mutex locking to ensure only one job processes a specific resource at a time. * @param job The job to process diff --git a/src/apps/app_sdk/JobUtils.ts b/src/apps/app_sdk/JobUtils.ts index c7f8b88a..2c94ef8e 100644 --- a/src/apps/app_sdk/JobUtils.ts +++ b/src/apps/app_sdk/JobUtils.ts @@ -60,8 +60,8 @@ base = base.replace(/\/+$/, ''); export function JobLink(job: Job): { text: string; url: string } { const text = `${job.queueName} => ${job.id}`; - const url = `${base}/jobs/queue/${encodeURIComponent(job.queueName)}/${ - job.id - }`; + const queue = encodeURIComponent(job.queueName); + const id = encodeURIComponent(job.id); + const url = `${base}/jobs/queue/${queue}/${id}`; return { text: text, url: url }; } diff --git a/src/apps/chatwoot/chatwoot.module.ts b/src/apps/chatwoot/chatwoot.module.ts index a1134aad..00ad4ce8 100644 --- a/src/apps/chatwoot/chatwoot.module.ts +++ b/src/apps/chatwoot/chatwoot.module.ts @@ -28,6 +28,7 @@ import { ChatWootScheduleService } from './services/ChatWootScheduleService'; import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService'; import { ChatWootConversationCreatedConsumer } from './consumers/inbox/conversation_created'; import { ChatWootConversationStatusChangedConsumer } from '@waha/apps/chatwoot/consumers/inbox/conversation_status_changed'; +import { TaskContactsPullConsumer } from '@waha/apps/chatwoot/consumers/task/contacts.pull'; const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController]; @@ -40,6 +41,10 @@ const IMPORTS = lodash.flatten([ name: QueueName.SCHEDULED_CHECK_VERSION, defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions), }), + RegisterAppQueue({ + name: QueueName.TASK_CONTACTS_PULL, + defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions), + }), RegisterAppQueue({ name: QueueName.WAHA_MESSAGE_ANY, defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions), @@ -98,14 +103,19 @@ const PROVIDERS = [ ChatWootConversationCreatedConsumer, ChatWootConversationStatusChangedConsumer, ChatWootInboxCommandsConsumer, + // Tasks + TaskContactsPullConsumer, + // WAHA WAHASessionStatusConsumer, WAHAMessageAnyConsumer, WAHAMessageReactionConsumer, WAHAMessageEditedConsumer, WAHAMessageRevokedConsumer, WAHAMessageAckConsumer, + // Scheduled MessageCleanupConsumer, CheckVersionConsumer, + // Services ChatWootWAHAQueueService, ChatWootQueueService, ChatWootScheduleService, diff --git a/src/apps/chatwoot/cli/cmd.contacts.ts b/src/apps/chatwoot/cli/cmd.contacts.ts new file mode 100644 index 00000000..5dd392f3 --- /dev/null +++ b/src/apps/chatwoot/cli/cmd.contacts.ts @@ -0,0 +1,79 @@ +import { CommandContext } from '@waha/apps/chatwoot/cli/types'; +import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName'; +import { + ContactsPullOptions, + ContactsPullStatusMessage, +} from '@waha/apps/chatwoot/consumers/task/contacts.pull'; +import { Job, JobsOptions, Queue } from 'bullmq'; +import { ChatWootScheduleService } from '@waha/apps/chatwoot/services/ChatWootScheduleService'; +import { ILogger } from '@waha/apps/app_sdk/ILogger'; +import { JobDataTimeout } from '@waha/apps/app_sdk/AppConsumer'; + +export async function ContactsPullStart( + ctx: CommandContext, + options: ContactsPullOptions, + jobOptions: JobsOptions & JobDataTimeout, +) { + const jobId = ChatWootScheduleService.SingleJobId(ctx.app); + const job: Job = await ctx.queues.importContacts.getJob(jobId); + if (job) { + const state = await job.getState(); + const done = state === 'completed' || state === 'failed'; + if (!done) { + const msg = ctx.l.r('cli.cmd.contacts.pull.already-running'); + await ctx.conversation.incoming(msg); + return; + } + + // Remove already done job + await ContactsPullRemove(ctx.queues.importContacts, ctx.app, ctx.logger); + } + + const opts: JobsOptions = { + ...jobOptions, + jobId: jobId, + }; + const data = { + app: ctx.app, + session: ctx.session, + timeout: { + job: jobOptions.timeout.job, + }, + options: options, + }; + await ctx.queues.importContacts.add(QueueName.TASK_CONTACTS_PULL, data, opts); + + const msg = ctx.l.r('cli.cmd.contacts.pull.queued', { + batch: options.batch, + }); + await ctx.conversation.incoming(msg); +} + +export async function ContactsPullStatus(ctx: CommandContext) { + const jobId = ChatWootScheduleService.SingleJobId(ctx.app); + const job: Job | null = await ctx.queues.importContacts.getJob(jobId); + + if (!job) { + const msg = ctx.l.r('cli.cmd.contacts.status.not-found'); + await ctx.conversation.incoming(msg); + return; + } + const state = await job.getState(); + const msg = ContactsPullStatusMessage(ctx.l, job, state); + await ctx.conversation.incoming(msg); +} + +export async function ContactsPullRemove( + queue: Queue, + app: string, + logger: ILogger, +): Promise { + const jobId = ChatWootScheduleService.SingleJobId(app); + const job: Job = await queue.getJob(jobId); + if (!job) { + logger.info('Pull Contacts job has already been removed'); + return false; + } + await job.remove(); + return true; +} diff --git a/src/apps/chatwoot/cli/index.ts b/src/apps/chatwoot/cli/index.ts index c07596e8..654d765e 100644 --- a/src/apps/chatwoot/cli/index.ts +++ b/src/apps/chatwoot/cli/index.ts @@ -1,7 +1,21 @@ -import { Command, CommanderError, OutputConfiguration } from 'commander'; +import { + Argument, + Command, + CommanderError, + Option, + OutputConfiguration, +} from 'commander'; import argvSplit from 'string-argv'; -import { BufferedOutput } from '@waha/apps/chatwoot/cli/BufferedOutput'; -import { buildFormatHelp, fullCommandPath } from '@waha/apps/chatwoot/cli/help'; +import { BufferedOutput } from '@waha/apps/chatwoot/cli/utils/BufferedOutput'; +import { + buildFormatHelp, + fullCommandPath, +} from '@waha/apps/chatwoot/cli/utils/help'; +import { + JobAttemptsOption, + JobTimeoutOption, + ParseMS, +} from '@waha/apps/chatwoot/cli/utils/options'; import { CommandContext } from '@waha/apps/chatwoot/cli/types'; import { SessionLogout, @@ -15,6 +29,13 @@ import { import { ServerReboot, ServerStatus } from '@waha/apps/chatwoot/cli/cmd.server'; import { ChatWootCommandsConfig } from '@waha/apps/chatwoot/dto/config.dto'; import { CommandDisabled } from '@waha/apps/chatwoot/cli/cmd.disabled'; +import { + ContactsPullStart, + ContactsPullStatus, +} from '@waha/apps/chatwoot/cli/cmd.contacts'; +import { ContactsPullOptions } from '@waha/apps/chatwoot/consumers/task/contacts.pull'; +import { JobsOptions } from 'bullmq'; +import { JobDataTimeout } from '@waha/apps/app_sdk/AppConsumer'; function BuildProgram( commands: ChatWootCommandsConfig, @@ -24,6 +45,7 @@ function BuildProgram( const l = ctx.l; const SessionGroup = l.r('cli.cmd.root.sub.session'); const ServerGroup = l.r('cli.cmd.root.sub.server'); + const SyncGroup = l.r('cli.cmd.root.sub.sync'); const program = new Command(); program @@ -31,13 +53,18 @@ function BuildProgram( .description(l.r('cli.cmd.root.description')) .exitOverride() .configureOutput(output) + .showSuggestionAfterError(true) .commandsGroup(SessionGroup) .commandsGroup(ServerGroup) + .commandsGroup(SyncGroup) .configureHelp({ helpWidth: 200, styleOptionTerm(str: string) { return `- \`${str}\``; }, + styleArgumentTerm(str: string) { + return `- **${str}**`; + }, styleUsage(str) { return `**${str}**`; }, @@ -109,6 +136,88 @@ function BuildProgram( .helpGroup(SessionGroup) .action(() => SessionScreenshot(ctx)); + // + // Pull Contacts + // + program + .command('contacts') + .alias('/contacts') + .summary(l.r('cli.cmd.contacts.summary')) + .description(l.r('cli.cmd.contacts.description')) + .helpGroup(SyncGroup) + .addArgument( + new Argument( + '[action]', + l.r('cli.cmd.contacts.action.description'), + ).choices(['pull', 'status']), + ) + .addOption( + new Option('--avatar ', l.r('cli.cmd.contacts.pull.option.avatar')) + .choices(['skip', 'if-missing', 'update']) + .default('if-missing'), + ) + .option('--groups', l.r('cli.cmd.contacts.pull.option.groups')) + .option('--no-lids', l.r('cli.cmd.contacts.pull.option.no-lids')) + .option( + '--no-attributes', + l.r('cli.cmd.contacts.pull.option.no-attributes'), + ) + .option( + '--batch ', + l.r('cli.cmd.contacts.pull.option.batch'), + 100 as any, + ) + .addOption(new JobAttemptsOption(l, 6)) + .addOption(new JobTimeoutOption(l, '10m')) + .addOption( + new Option( + '--delay-contact ', + l.r('cli.cmd.contacts.pull.option.delay-contact'), + ) + .argParser(ParseMS) + .default(ParseMS('0.1s'), '0.1s'), + ) + .addOption( + new Option( + '--delay-batch ', + l.r('cli.cmd.contacts.pull.option.delay-batch'), + ) + .argParser(ParseMS) + .default(ParseMS('1s'), '1s'), + ) + .action(async (action, opts, cmd: Command) => { + if (!action) { + cmd.outputHelp(); + return; + } + + if (action === 'status') { + await ContactsPullStatus(ctx); + return; + } + + const options: ContactsPullOptions = { + batch: opts.batch, + avatar: opts.avatar, + attributes: opts.attributes, + contacts: { + lids: opts.lids, + groups: opts.groups, + }, + delay: { + contact: opts.delayContact, + batch: opts.delayBatch, + }, + }; + const jobOptions: JobsOptions & JobDataTimeout = { + attempts: opts.attempts, + timeout: { + job: opts.timeout, + }, + }; + await ContactsPullStart(ctx, options, jobOptions); + }); + // // Server // diff --git a/src/apps/chatwoot/cli/types.ts b/src/apps/chatwoot/cli/types.ts index 3d118a22..c02b24b6 100644 --- a/src/apps/chatwoot/cli/types.ts +++ b/src/apps/chatwoot/cli/types.ts @@ -2,11 +2,16 @@ import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf'; import { Locale } from '@waha/apps/chatwoot/i18n/locale'; import { Conversation } from '../client/Conversation'; import { ILogger } from '@waha/apps/app_sdk/ILogger'; +import { Queue } from 'bullmq'; export interface CommandContext { + app: string; + session: string; logger: ILogger; l: Locale; - session: string; waha: WAHASelf; conversation: Conversation; + queues: { + importContacts: Queue; + }; } diff --git a/src/apps/chatwoot/cli/BufferedOutput.ts b/src/apps/chatwoot/cli/utils/BufferedOutput.ts similarity index 100% rename from src/apps/chatwoot/cli/BufferedOutput.ts rename to src/apps/chatwoot/cli/utils/BufferedOutput.ts diff --git a/src/apps/chatwoot/cli/help.ts b/src/apps/chatwoot/cli/utils/help.ts similarity index 100% rename from src/apps/chatwoot/cli/help.ts rename to src/apps/chatwoot/cli/utils/help.ts diff --git a/src/apps/chatwoot/cli/utils/options.ts b/src/apps/chatwoot/cli/utils/options.ts new file mode 100644 index 00000000..7a898515 --- /dev/null +++ b/src/apps/chatwoot/cli/utils/options.ts @@ -0,0 +1,33 @@ +import * as ms from 'ms'; +import { Option } from 'commander'; +import { Locale } from '@waha/apps/chatwoot/i18n/locale'; + +/** + * Parse human string into milliseconds + */ +export function ParseMS(value: string) { + const duration = ms(value); + if (duration == null) throw new Error(`Invalid duration: "${value}"`); + if (duration < 0) throw new Error(`Duration cannot be negative: "${value}"`); + return duration; +} + +export function ParseSeconds(value: string): number { + const duration = ParseMS(value); + return Math.floor(duration / 1000); +} + +export class JobAttemptsOption extends Option { + constructor(l: Locale, def: number) { + super('--attempts ', l.r('cli.cmd.options.job.attempts')); + this.default(def); + } +} + +export class JobTimeoutOption extends Option { + constructor(l: Locale, def: string) { + super('--timeout ', l.r('cli.cmd.options.job.timeout')); + this.argParser(ParseMS); + this.default(ParseMS(def)); + } +} diff --git a/src/apps/chatwoot/client/ContactConversationService.ts b/src/apps/chatwoot/client/ContactConversationService.ts index 02a68c40..9cb40da3 100644 --- a/src/apps/chatwoot/client/ContactConversationService.ts +++ b/src/apps/chatwoot/client/ContactConversationService.ts @@ -3,7 +3,10 @@ import ChatwootClient, { public_contact_create_update_payload, } from '@figuro/chatwoot-sdk'; import { ILogger } from '@waha/apps/app_sdk/ILogger'; -import { ContactService } from '@waha/apps/chatwoot/client/ContactService'; +import { + AvatarUpdateMode, + ContactService, +} from '@waha/apps/chatwoot/client/ContactService'; import { Conversation } from '@waha/apps/chatwoot/client/Conversation'; import { ContactIds, @@ -40,7 +43,7 @@ export class ContactConversationService { this.cache = CacheForConfig(config); } - private async upsertByContactInfo( + public async upsertByContactInfo( contactInfo: ContactInfo, ): Promise { const chatId = contactInfo.ChatId(); @@ -50,55 +53,41 @@ export class ContactConversationService { return this.cache.get(chatId); } - // - // Find or create contact - // - let contact = await this.contactService.searchByAnyID(chatId); - if (!contact) { - const request = await contactInfo.PublicContactCreate(); - contact = await this.contactService.create(chatId, request); - } + let cwContact = await this.contactService.findOrCreateContact(contactInfo); // Update custom attributes - always - const attributes = await contactInfo.Attributes(); this.logger.info( - `Updating if required contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`, + `Updating if required contact custom attributes for chat.id: ${chatId}, contact.id: ${cwContact.data.id}`, + ); + const attributes = await contactInfo.Attributes(); + await this.contactService.upsertCustomAttributes( + cwContact.data, + attributes, + ); + await this.contactService.updateAvatar( + cwContact, + contactInfo, + AvatarUpdateMode.IF_MISSING, ); - await this.contactService.upsertCustomAttributes(contact.data, attributes); - - // Update Avatar if nothing, but keep the original one if any - if (!contact.data.thumbnail) { - const avatarUrl = await contactInfo.AvatarUrl().catch((err) => { - this.logger.warn( - `Error getting avatar for chat.id from WhatsApp: ${chatId}`, - ); - this.logger.warn(err); - return null; - }); - if (avatarUrl) { - this.contactService.updateAvatarUrlSafe(contact.data.id, avatarUrl); - } - } - this.logger.info( - `Using contact for chat.id: ${chatId}, contact.id: ${contact.data.id}, contact.sourceId: ${contact.sourceId}`, + `Using contact for chat.id: ${chatId}, contact.id: ${cwContact.data.id}, contact.sourceId: ${cwContact.sourceId}`, ); // // Get or create a conversation for this inbox // const conversation = await this.conversationService.upsert({ - id: contact.data.id, - sourceId: contact.sourceId, + id: cwContact.data.id, + sourceId: cwContact.sourceId, }); this.logger.info( - `Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${contact.sourceId}`, + `Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${cwContact.sourceId}`, ); // Save to cache const ids = { id: conversation.id, - sourceId: contact.sourceId, + sourceId: cwContact.sourceId, }; this.cache.set(chatId, ids); return ids; diff --git a/src/apps/chatwoot/client/ContactService.ts b/src/apps/chatwoot/client/ContactService.ts index cf588861..4a5ba0f7 100644 --- a/src/apps/chatwoot/client/ContactService.ts +++ b/src/apps/chatwoot/client/ContactService.ts @@ -14,12 +14,18 @@ import * as lodash from 'lodash'; import { AttributeKey } from '../const'; import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer'; +import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService'; export interface ContactResponse { data: generic_id & contact; sourceId: string; } +export enum AvatarUpdateMode { + IF_MISSING, + ALWAYS, +} + export class ContactService { constructor( private config: ChatWootAPIConfig, @@ -28,6 +34,16 @@ export class ContactService { private logger: ILogger, ) {} + async findOrCreateContact(contactInfo: ContactInfo) { + const chatId = contactInfo.ChatId(); + let contact = await this.searchByAnyID(chatId); + if (!contact) { + const request = await contactInfo.PublicContactCreate(); + contact = await this.create(chatId, request); + } + return contact; + } + async searchByAnyID(chatId: string): Promise { const payload: any[] = [ { @@ -136,6 +152,28 @@ export class ContactService { }; } + public async updateAvatar( + contact: ContactResponse, + contactInfo: ContactInfo, + mode: AvatarUpdateMode, + ) { + // Update Avatar if nothing, but keep the original one if any + if (contact.data.thumbnail && mode == AvatarUpdateMode.IF_MISSING) { + return; + } + const chatId = contactInfo.ChatId(); + const avatarUrl = await contactInfo.AvatarUrl().catch((err) => { + this.logger.warn( + `Error getting avatar for chat.id from WhatsApp: ${chatId}`, + ); + this.logger.warn(err); + return null; + }); + if (avatarUrl) { + this.updateAvatarUrlSafe(contact.data.id, avatarUrl); + } + } + public updateAvatarUrlSafe(contactId, avatarUrl: string) { this.accountAPI.contacts .update({ diff --git a/src/apps/chatwoot/consumers/QueueName.ts b/src/apps/chatwoot/consumers/QueueName.ts index 14cd256d..bc76ce23 100644 --- a/src/apps/chatwoot/consumers/QueueName.ts +++ b/src/apps/chatwoot/consumers/QueueName.ts @@ -5,6 +5,11 @@ export enum QueueName { SCHEDULED_MESSAGE_CLEANUP = 'chatwoot.scheduled | message.cleanup', SCHEDULED_CHECK_VERSION = 'chatwoot.scheduled | check.version', + // + // Task + // + TASK_CONTACTS_PULL = 'chatwoot.task | contacts.pull', + // // WAHA Events // diff --git a/src/apps/chatwoot/consumers/inbox/commands.ts b/src/apps/chatwoot/consumers/inbox/commands.ts index bc4ab1ed..350ea8d8 100644 --- a/src/apps/chatwoot/consumers/inbox/commands.ts +++ b/src/apps/chatwoot/consumers/inbox/commands.ts @@ -1,11 +1,11 @@ -import { Processor } from '@nestjs/bullmq'; +import { InjectQueue, Processor } from '@nestjs/bullmq'; import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants'; import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base'; import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName'; import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer'; import { SessionManager } from '@waha/core/abc/manager.abc'; import { RMutexService } from '@waha/modules/rmutex/rmutex.service'; -import { Job } from 'bullmq'; +import { Job, Queue } from 'bullmq'; import { PinoLogger } from 'nestjs-pino'; import { TKey } from '@waha/apps/chatwoot/i18n/templates'; import { runText } from '@waha/apps/chatwoot/cli'; @@ -17,6 +17,8 @@ export class ChatWootInboxCommandsConsumer extends ChatWootInboxMessageConsumer protected readonly manager: SessionManager, log: PinoLogger, rmutex: RMutexService, + @InjectQueue(QueueName.TASK_CONTACTS_PULL) + private readonly importContactsQueue: Queue, ) { super(manager, log, rmutex, 'ChatWootInboxCommandsConsumer'); } @@ -26,19 +28,23 @@ export class ChatWootInboxCommandsConsumer extends ChatWootInboxMessageConsumer } protected async Process(container: DIContainer, body, job: Job) { - const session = job.data.session; const cmd = body.content; - - this.logger.info(`Executing command '${cmd}' for session ${session}...`); + this.logger.info( + `Executing command '${cmd}' for session ${job.data.session}...`, + ); const repo = container.ContactConversationService(); const conversation = await repo.InboxNotifications(); const ctx: CommandContext = { + app: job.data.app, + session: job.data.session, logger: this.logger, l: container.Locale(), - session: session, waha: container.WAHASelf(), conversation: conversation, + queues: { + importContacts: this.importContactsQueue, + }, }; const commands = container.ChatWootConfig().commands; const output = await runText(commands, ctx, cmd); diff --git a/src/apps/chatwoot/consumers/scheduled/base.ts b/src/apps/chatwoot/consumers/scheduled/base.ts index 5878ea36..20d95457 100644 --- a/src/apps/chatwoot/consumers/scheduled/base.ts +++ b/src/apps/chatwoot/consumers/scheduled/base.ts @@ -4,7 +4,6 @@ import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils'; import { MessageType } from '@waha/apps/chatwoot/client/types'; import { ScheduledData } from '@waha/apps/chatwoot/consumers/types'; import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer'; -import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto'; import { SessionManager } from '@waha/core/abc/manager.abc'; import { RMutexService } from '@waha/modules/rmutex/rmutex.service'; import { Job } from 'bullmq'; diff --git a/src/apps/chatwoot/consumers/task/base.ts b/src/apps/chatwoot/consumers/task/base.ts new file mode 100644 index 00000000..50761165 --- /dev/null +++ b/src/apps/chatwoot/consumers/task/base.ts @@ -0,0 +1,109 @@ +import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer'; +import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper'; +import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils'; +import { MessageType } from '@waha/apps/chatwoot/client/types'; +import { ScheduledData } from '@waha/apps/chatwoot/consumers/types'; +import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer'; +import { SessionManager } from '@waha/core/abc/manager.abc'; +import { RMutexService } from '@waha/modules/rmutex/rmutex.service'; +import { Job } from 'bullmq'; +import { PinoLogger } from 'nestjs-pino'; + +import { AppRepository } from '../../storage'; +import { TKey } from '@waha/apps/chatwoot/i18n/templates'; +import { SignalRace } from '@waha/utils/abortable'; + +/** + * Base class for ChatWoot background task consumers + */ +export abstract class ChatWootTaskConsumer extends AppConsumer { + protected appRepository: AppRepository; + + constructor( + protected readonly manager: SessionManager, + log: PinoLogger, + rmutex: RMutexService, + protected readonly consumerName: string, + ) { + super('ChatWoot', consumerName, log, rmutex); + } + + /** + * Gets the DIContainer for the specified app. + */ + protected async DIContainer(job: Job, appId: string): Promise { + const knex = this.manager.store.getWAHADatabase(); + this.appRepository = new AppRepository(knex); + const logger = new JobLoggerWrapper(job, this.logger); + const app = await this.appRepository.getById(appId); + return new DIContainer(app.pk, app.config, logger, knex); + } + + protected abstract ErrorHeaderKey(): TKey | null; + + /** + * Get the function that processes the scheduled job + * This method must be implemented by subclasses + */ + protected abstract Process( + container: DIContainer, + job: Job, + signal: AbortSignal, + ): Promise; + + /** + * Process the job + * This method is called by the queue processor + */ + async processJob(job: Job): Promise { + const signal = this.signal(job); + return SignalRace(this.ProcessAndReportStatus(job, signal), signal); + } + + private async ProcessAndReportStatus(job: Job, signal: AbortSignal) { + try { + const container = await this.DIContainer(job, job.data.app); + const result = await this.Process(container, job, signal); + await this.ReportErrorRecovered(job); + return result; + } catch (err) { + this.ReportErrorForJob(job, err).catch((exc) => + this.logger.error(`Failed to report error for job ${job.id}: ${exc}`), + ); + throw err; + } + } + + /** + * Report an error for a scheduled job + */ + protected async ReportErrorForJob(job: Job, err: any) { + const container = await this.DIContainer(job, job.data.app); + let header = this.ErrorHeaderKey() + ? container.Locale().key(this.ErrorHeaderKey()).render() + : err.message || `${err}`; + header = `${job.queueName}: ${header}`; + + const conversation = await container + .ContactConversationService() + .InboxNotifications(); + + const reporter = container.ChatWootErrorReporter(job); + await reporter.ReportError(conversation, header, MessageType.INCOMING, err); + throw err; + } + + protected async ReportErrorRecovered(job: Job) { + if (!HasBeenRetried(job)) { + return; + } + + const container = await this.DIContainer(job, job.data.app); + const conversation = await container + .ContactConversationService() + .InboxNotifications(); + + const reporter = container.ChatWootErrorReporter(job); + await reporter.ReportSucceeded(conversation, MessageType.INCOMING); + } +} diff --git a/src/apps/chatwoot/consumers/task/contacts.pull.ts b/src/apps/chatwoot/consumers/task/contacts.pull.ts new file mode 100644 index 00000000..2e6b1151 --- /dev/null +++ b/src/apps/chatwoot/consumers/task/contacts.pull.ts @@ -0,0 +1,245 @@ +import { Processor } from '@nestjs/bullmq'; +import * as lodash from 'lodash'; +import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants'; +import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName'; +import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer'; +import { SessionManager } from '@waha/core/abc/manager.abc'; +import { RMutexService } from '@waha/modules/rmutex/rmutex.service'; +import { Job, JobState } from 'bullmq'; +import { PinoLogger } from 'nestjs-pino'; + +import { ChatWootTaskConsumer } from '@waha/apps/chatwoot/consumers/task/base'; +import { TKey } from '@waha/apps/chatwoot/i18n/templates'; +import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf'; +import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo'; +import { ContactSortField } from '@waha/structures/contacts.dto'; +import { SortOrder } from '@waha/structures/pagination.dto'; +import { + AvatarUpdateMode, + ContactService, +} from '@waha/apps/chatwoot/client/ContactService'; +import { Conversation } from '@waha/apps/chatwoot/client/Conversation'; +import { Locale } from '@waha/apps/chatwoot/i18n/locale'; +import { ILogger } from '@waha/apps/app_sdk/ILogger'; +import { JobLink } from '@waha/apps/app_sdk/JobUtils'; +import { sleep } from '@waha/utils/promiseTimeout'; +import { isJidGroup, isPnUser, isLidUser } from '@waha/core/utils/jids'; + +export interface ContactsPullOptions { + batch: number; + avatar: 'skip' | 'if-missing' | 'update'; + attributes: boolean; + contacts: { + groups: boolean; + lids: boolean; + }; + delay: { + contact: number; + batch: number; + }; +} + +/** + * Task consumer that pulls WhatsApp contacts into ChatWoot + * Runs in batches to keep the job resumable and observable in ChatWoot + */ +@Processor(QueueName.TASK_CONTACTS_PULL, { concurrency: JOB_CONCURRENCY }) +export class TaskContactsPullConsumer extends ChatWootTaskConsumer { + constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) { + super(manager, log, rmutex, TaskContactsPullConsumer.name); + } + + protected ErrorHeaderKey(): TKey { + return null; + } + + /** + * Process the contact pull job + * Streams batches of contacts from WAHA into ChatWoot storage + */ + protected async Process( + container: DIContainer, + job: Job, + signal?: AbortSignal, + ): Promise { + const options: ContactsPullOptions = job.data.options; + const locale = container.Locale(); + const waha = container.WAHASelf(); + const session = new WAHASessionAPI(job.data.session, waha); + const conversation = await container + .ContactConversationService() + .InboxNotifications(); + + const handler = new ContactsPullHandler( + signal, + container.Logger(), + conversation, + session, + locale, + container.ContactService(), + ); + return await handler.handle(options, job); + } +} + +interface Progress { + ok: number; + errors: number; + skipped: number; +} + +const NullProgress: Progress = { + ok: 0, + errors: 0, + skipped: 0, +}; + +class ContactsPullHandler { + constructor( + private signal: AbortSignal, + private logger: ILogger, + private conversation: Conversation, + private session: WAHASessionAPI, + private l: Locale, + private contactService: ContactService, + ) {} + + async handle(options: ContactsPullOptions, job) { + const batch = options.batch; + let progress = lodash.merge({}, NullProgress, job.progress); + this.logger.info( + `Pulling contacts for session ${job.data.session} with batch size ${batch}...`, + ); + if (progress.ok == 0 && progress.errors == 0 && progress.skipped == 0) { + await this.conversation.activity( + this.l.r('task.contacts.started', { progress: progress }), + ); + } else { + await this.conversation.activity( + this.l.r('task.contacts.progress', { progress: progress }), + ); + } + + // fake 'null' first, so it's not empty list + // we overwrite it at the beginning of the loop + let contacts: any[] = [null]; + while (contacts.length != 0) { + this.signal.throwIfAborted(); + const processed = progress.ok + progress.errors + progress.skipped; + this.logger.info( + `Fetching contacts batch: offset=${processed}, limit=${batch}...`, + ); + contacts = await this.session.getContacts( + { + offset: processed, + limit: batch, + sortBy: ContactSortField.ID, + sortOrder: SortOrder.ASC, + }, + { signal: this.signal }, + ); + + for (const contact of contacts) { + try { + const chatId = contact.id; + if (isJidGroup(chatId)) { + if (!options.contacts.groups) { + this.logger.info(`Skipping group contact ${chatId}...`); + progress.skipped += 1; + await job.updateProgress(progress); + continue; + } + } else if (isLidUser(chatId)) { + if (!options.contacts.lids) { + this.logger.info(`Skipping PN contact ${chatId}...`); + progress.skipped += 1; + await job.updateProgress(progress); + continue; + } + this.logger.info(`Skipping LID contact ${chatId}...`); + } else if (!isPnUser(chatId)) { + this.logger.info(`Skipping non-phone-number contact ${chatId}...`); + progress.skipped += 1; + await job.updateProgress(progress); + continue; + } + + this.logger.info(`Pulling ${chatId}...`); + await this.pullOneContact(options, chatId); + progress.ok += 1; + } catch (e) { + this.logger.error(`Error pulling contact ${contact.id}: ${e}`); + progress.errors += 1; + continue; + } + // update progress before signal fails + await job.updateProgress(progress); + this.signal.throwIfAborted(); + await sleep(options.delay.contact); + } + await this.conversation.activity( + this.l.r('task.contacts.progress', { progress: progress }), + ); + await sleep(options.delay.batch); + } + + // + // Final report + // + job.progress = progress; + const msg = ContactsPullStatusMessage(this.l, job, 'completed'); + await this.conversation.incoming(msg); + } + + private async pullOneContact(options: ContactsPullOptions, chatId: string) { + // Contact + const contactInfo = WhatsAppContactInfo(this.session, chatId, this.l); + let cwContact = await this.contactService.findOrCreateContact(contactInfo); + // Attributes + if (options.attributes) { + const attributes = await contactInfo.Attributes(); + await this.contactService.upsertCustomAttributes( + cwContact.data, + attributes, + ); + } + // Avatar + switch (options.avatar) { + case 'if-missing': + await this.contactService.updateAvatar( + cwContact, + contactInfo, + AvatarUpdateMode.IF_MISSING, + ); + break; + case 'update': + await this.contactService.updateAvatar( + cwContact, + contactInfo, + AvatarUpdateMode.ALWAYS, + ); + break; + } + } +} + +export function ContactsPullStatusMessage( + l: Locale, + job, + state: JobState | 'unknown', +) { + const details = JobLink(job); + job.progress = lodash.merge({}, NullProgress, job.progress); + const payload = { + error: state === 'failed' || job.progress.errors > 0, + state: lodash.capitalize(state), + progress: job.progress, + details: details, + job: { + timestamp: l.FormatTimestampSec(job.timestamp), + processedOn: l.FormatTimestampSec(job.processedOn), + finishedOn: l.FormatTimestampSec(job.finishedOn), + }, + }; + return l.r('task.contacts.status', payload); +} diff --git a/src/apps/chatwoot/di/DIContainer.ts b/src/apps/chatwoot/di/DIContainer.ts index 03a60a8b..4c663802 100644 --- a/src/apps/chatwoot/di/DIContainer.ts +++ b/src/apps/chatwoot/di/DIContainer.ts @@ -109,7 +109,7 @@ export class DIContainer { * @returns ContactService instance */ @CacheSync() - private ContactService(): ContactService { + public ContactService(): ContactService { return new ContactService( this.config, this.AccountAPI(), diff --git a/src/apps/chatwoot/i18n/locale.ts b/src/apps/chatwoot/i18n/locale.ts index 0e56819b..fb886913 100644 --- a/src/apps/chatwoot/i18n/locale.ts +++ b/src/apps/chatwoot/i18n/locale.ts @@ -64,7 +64,22 @@ export class Locale { return date.toLocaleString(this.locale, options); } - FormatTimestamp(timestamp: Long | string | number | null): string | null { + FormatDatetimeSec(date: Date | null) { + if (!date) { + return null; + } + const options: any = this.strings['datetime'] || {}; + options.second = '2-digit'; + options.timeZone = options.timeZone || options.timezone || process.env.TZ; + return date.toLocaleString(this.locale, options); + } + + FormatTimestampSec(timestamp: any) { + const date = this.ParseTimestamp(timestamp); + return this.FormatDatetimeSec(date); + } + + ParseTimestamp(timestamp: Long | string | number | null): Date | null { const value = ensureNumber(timestamp); if (!value) { return undefined; @@ -77,6 +92,11 @@ export class Locale { if (Number.isNaN(date.getTime())) { return undefined; } + return date; + } + + FormatTimestamp(timestamp: Long | string | number | null): string | null { + const date = this.ParseTimestamp(timestamp); return this.FormatDatetime(date); } } diff --git a/src/apps/chatwoot/i18n/locales/ar-AE.yaml b/src/apps/chatwoot/i18n/locales/ar-AE.yaml index 2d4773a6..7180099c 100644 --- a/src/apps/chatwoot/i18n/locales/ar-AE.yaml +++ b/src/apps/chatwoot/i18n/locales/ar-AE.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ تم تحديث إعدادات جلسة واتساب **{{{name}}}**. +cli.usage.arguments: |- + [الإجراء] + cli.usage.command: |- [الأمر] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **الخادم**: +cli.cmd.root.sub.sync: |- + 🔄️ **المزامنة**: + cli.cmd.session.status.description: |- اطّلع على حالة الجلسة الحالية. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- التقط لقطة شاشة للجلسة (متاح فقط مع محرك **WEBJS**). +cli.cmd.contacts.summary: |- + اسحب جهات اتصال واتساب. + +cli.cmd.contacts.description: |- + 👤 شغِّل `contacts pull` لبدء المزامنة أو `contacts status` لمتابعة التقدم. + +cli.cmd.contacts.action.description: |- + استخدم `start` لبدء سحب جهات الاتصال أو `status` لمراجعة المهمة النشطة. + +cli.cmd.contacts.pull.description: |- + ابدأ بسحب جهات اتصال واتساب إلى ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + عدد جهات الاتصال التي يتم سحبها في كل دفعة. + +cli.cmd.contacts.pull.option.delay-contact: |- + التأخير بين مزامنة كل جهة اتصال لتجنّب حدود معدل سحب الصور. + +cli.cmd.contacts.pull.option.delay-batch: |- + التأخير بين دفعات جهات الاتصال. + +cli.cmd.contacts.pull.option.avatar: |- + اختر كيفية سحب الصور الرمزية: `skip` أو `if-missing` أو `update`. + +cli.cmd.contacts.pull.option.groups: |- + قم بتضمين جهات اتصال مجموعات واتساب في المزامنة. + +cli.cmd.contacts.pull.option.no-lids: |- + تخطَّ جهات اتصال واتساب LID (معرّفات الأجهزة المرتبطة JID) في المزامنة. + +cli.cmd.contacts.pull.option.no-attributes: |- + لا تسحب سمات جهة الاتصال إذا كانت موجودة بالفعل (هذا يسرّع العملية). + +cli.cmd.contacts.pull.already-running: |- + ⏳ مهمة **سحب جهات الاتصال** تعمل بالفعل. انتظر حتى تنتهي أو أوقفها قبل بدء واحدة جديدة. + + أرسل `contacts status` للاطلاع على التفاصيل. + +cli.cmd.contacts.pull.queued: |- + 👥 تمت جدولة مهمة **سحب جهات الاتصال**. + + سنشارك تحديث التقدم كل **{{batch}}** جهة اتصال. + قد يستغرق سحب جهات الاتصال بعض الوقت، لذا تحقق من آخر حالة في أي وقت باستخدام `contacts status`. + +cli.cmd.options.job.attempts: |- + عدد مرات إعادة محاولة المهمة قبل اعتبارها فاشلة. + +cli.cmd.options.job.timeout: |- + الحد الأقصى لمدة تشغيل المهمة قبل إعادة المحاولة (يدعم قيمًا مثل `30s` أو `5m` أو `1h`). + +cli.cmd.contacts.status.description: |- + اعرض حالة مهمة السحب الحالية. + +cli.cmd.contacts.status.not-found: |- + ℹ️ لم يتم العثور على مهمة **سحب جهات الاتصال** نشطة. ابدأ واحدة باستخدام `contacts pull`. + cli.cmd.server.description: |- أدِر الخادم. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **الخيارات العامة:** +task.contacts.status: |- + 👤 سحب جهات الاتصال: **{{state}}** + - ✅ الناجحة: {{{progress.ok}}} جهة اتصال + - ⚪ تم تخطيها: {{{progress.skipped}}} جهة اتصال + - ⚠️ الأخطاء: {{{progress.errors}}} جهة اتصال + + {{#job}} + ⌛ الجدول الزمني: + - في قائمة الانتظار: {{{timestamp}}} + {{#processedOn}} + - وقت البدء: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - وقت الانتهاء: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + التفاصيل: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + جارٍ سحب جهات الاتصال... + +task.contacts.progress: |- + سحب جهات الاتصال: ✅ {{progress.ok}}، ⚪ {{progress.skipped}}، ⚠️ {{progress.errors}} + app.help.reminder: |- أرسل **help** للحصول على قائمة الأوامر المتاحة. diff --git a/src/apps/chatwoot/i18n/locales/bn-BD.yaml b/src/apps/chatwoot/i18n/locales/bn-BD.yaml index 956e8da5..0e4e5878 100644 --- a/src/apps/chatwoot/i18n/locales/bn-BD.yaml +++ b/src/apps/chatwoot/i18n/locales/bn-BD.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ হোয়াটসঅ্যাপ সেশন **{{{name}}}** এর সেটিংস আপডেট করা হয়েছে। +cli.usage.arguments: |- + [অ্যাকশন] + cli.usage.command: |- [কমান্ড] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **সার্ভার**: +cli.cmd.root.sub.sync: |- + 🔄️ **সিঙ্ক**: + cli.cmd.session.status.description: |- বর্তমান সেশনের স্ট্যাটাস দেখুন। @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- সেশনের স্ক্রিনশট নিন (শুধুমাত্র **WEBJS** ইঞ্জিনে উপলব্ধ)। +cli.cmd.contacts.summary: |- + হোয়াটসঅ্যাপ কনট্যাক্টস আনুন। + +cli.cmd.contacts.description: |- + 👤 সিঙ্ক শুরু করতে `contacts pull` চালান বা অগ্রগতি দেখতে `contacts status` চালান। + +cli.cmd.contacts.action.description: |- + `start` দিয়ে কনট্যাক্ট টানা শুরু করুন বা `status` দিয়ে সক্রিয় কাজ পরীক্ষা করুন। + +cli.cmd.contacts.pull.description: |- + হোয়াটসঅ্যাপ কনট্যাক্টস ChatWoot-এ আনা শুরু করুন। + +cli.cmd.contacts.pull.option.batch: |- + প্রতি ব্যাচে কত কনট্যাক্ট টানা হবে। + +cli.cmd.contacts.pull.option.delay-contact: |- + প্রতিটি কনট্যাক্ট সিঙ্কের মাঝে বিলম্ব, যাতে অ্যাভাটার টানার সময় রেট লিমিট এড়ানো যায়। + +cli.cmd.contacts.pull.option.delay-batch: |- + প্রতিটি ব্যাচের মাঝে বিলম্ব। + +cli.cmd.contacts.pull.option.avatar: |- + কনট্যাক্টের অ্যাভাটার কীভাবে টানবেন নির্ধারণ করুন: `skip`, `if-missing`, অথবা `update`। + +cli.cmd.contacts.pull.option.groups: |- + সিঙ্কে WhatsApp গ্রুপ কনট্যাক্ট অন্তর্ভুক্ত করুন। + +cli.cmd.contacts.pull.option.no-lids: |- + সিঙ্কে WhatsApp LID কনট্যাক্ট (লিঙ্কড-ডিভাইস JID) বাদ দিন। + +cli.cmd.contacts.pull.option.no-attributes: |- + কনট্যাক্ট আগেই থাকলে তার অ্যাট্রিবিউট টানবেন না (এতে প্রক্রিয়া দ্রুত হয়)। + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** কাজটি ইতিমধ্যেই চলছে। এটি শেষ হওয়া পর্যন্ত অপেক্ষা করুন বা নতুন কাজ শুরু করার আগে থামান। + + বিস্তারিত জানতে `contacts status` পাঠান। + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** কাজটি কিউতে রয়েছে। + + আমরা প্রতি **{{batch}}** কনট্যাক্টে অগ্রগতির আপডেট শেয়ার করব। + কনট্যাক্ট টানতে কিছুটা সময় লাগতে পারে - যেকোনো সময় `contacts status` দিয়ে সর্বশেষ অবস্থা দেখুন। + +cli.cmd.options.job.attempts: |- + কোনো কাজ ব্যর্থ ঘোষণা করার আগে কয়বার পুনরায় চেষ্টা করা হবে। + +cli.cmd.options.job.timeout: |- + কাজ পুনরায় চেষ্টা করার আগে সর্বোচ্চ চালনার সময় (যেমন `30s`, `5m`, `1h`)। + +cli.cmd.contacts.status.description: |- + চলমান টানার কাজের অবস্থা দেখান। + +cli.cmd.contacts.status.not-found: |- + ℹ️ কোনো সক্রিয় **Pull Contacts** কাজ পাওয়া যায়নি। `contacts pull` দিয়ে একটি শুরু করুন। + cli.cmd.server.description: |- সার্ভার পরিচালনা করুন। @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **গ্লোবাল বিকল্পসমূহ:** +task.contacts.status: |- + 👤 কনট্যাক্ট টানা: **{{state}}** + - ✅ সফল: {{{progress.ok}}} কনট্যাক্ট + - ⚪ এড়িয়ে গেছে: {{{progress.skipped}}} কনট্যাক্ট + - ⚠️ ত্রুটি: {{{progress.errors}}} কনট্যাক্ট + + {{#job}} + ⌛ সময়রেখা: + - কিউ হয়েছে: {{{timestamp}}} + {{#processedOn}} + - শুরু হয়েছে: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - শেষ হয়েছে: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + বিস্তারিত: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + কনট্যাক্ট টানা হচ্ছে... + +task.contacts.progress: |- + কনট্যাক্ট টানা: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- উপলব্ধ কমান্ডগুলির তালিকা পেতে **help** পাঠান। diff --git a/src/apps/chatwoot/i18n/locales/de-DE.yaml b/src/apps/chatwoot/i18n/locales/de-DE.yaml index d171e17d..74895b6f 100644 --- a/src/apps/chatwoot/i18n/locales/de-DE.yaml +++ b/src/apps/chatwoot/i18n/locales/de-DE.yaml @@ -120,6 +120,9 @@ app.updated.message: |- ✅ Einstellungen für WhatsApp-Sitzung **{{{name}}}** wurden aktualisiert. +cli.usage.arguments: |- + [Aktion] + cli.usage.command: |- [Befehl] @@ -141,6 +144,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Server**: +cli.cmd.root.sub.sync: |- + 🔄️ **Synchronisierung**: + cli.cmd.session.status.description: |- Aktuellen Sitzungsstatus abrufen. @@ -162,6 +168,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Einen Screenshot der Sitzung aufnehmen (nur mit der **WEBJS**-Engine verfügbar). +cli.cmd.contacts.summary: |- + WhatsApp-Kontakte abrufen. + +cli.cmd.contacts.description: |- + 👤 Führen Sie `contacts pull` aus, um eine Synchronisierung zu starten, oder `contacts status`, um den Fortschritt zu prüfen. + +cli.cmd.contacts.action.description: |- + Mit `start` den Abruf beginnen oder mit `status` den aktiven Auftrag prüfen. + +cli.cmd.contacts.pull.description: |- + Beginnen Sie, WhatsApp-Kontakte in ChatWoot zu importieren. + +cli.cmd.contacts.pull.option.batch: |- + Anzahl der Kontakte pro Batch. + +cli.cmd.contacts.pull.option.delay-contact: |- + Verzögerung zwischen einzelnen Kontakten, um beim Laden von Avataren Rate Limits zu vermeiden. + +cli.cmd.contacts.pull.option.delay-batch: |- + Verzögerung zwischen Kontakt-Batches. + +cli.cmd.contacts.pull.option.avatar: |- + Legen Sie fest, wie Avatare geladen werden: `skip`, `if-missing` oder `update`. + +cli.cmd.contacts.pull.option.groups: |- + WhatsApp-Gruppenkontakte in die Synchronisierung einbeziehen. + +cli.cmd.contacts.pull.option.no-lids: |- + WhatsApp-LID-Kontakte (Linked-Device-JIDs) bei der Synchronisierung überspringen. + +cli.cmd.contacts.pull.option.no-attributes: |- + Keine Kontaktattribute laden, wenn der Kontakt bereits existiert (beschleunigt den Vorgang). + +cli.cmd.contacts.pull.already-running: |- + ⏳ Der Auftrag **Pull Contacts** läuft bereits. Warten Sie, bis er abgeschlossen ist, oder stoppen Sie ihn, bevor Sie einen neuen starten. + + Senden Sie `contacts status` für Details. + +cli.cmd.contacts.pull.queued: |- + 👥 Auftrag **Pull Contacts** in die Warteschlange gestellt. + + Wir teilen nach jeweils **{{batch}}** Kontakten ein Fortschrittsupdate. + Das Abrufen kann etwas dauern – prüfen Sie den aktuellen Stand jederzeit mit `contacts status`. + +cli.cmd.options.job.attempts: |- + Anzahl der erneuten Versuche, bevor der Auftrag als fehlgeschlagen markiert wird. + +cli.cmd.options.job.timeout: |- + Maximale Laufzeit, bevor der Auftrag erneut versucht wird (unterstützt Werte wie `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Status des aktuellen Abrufauftrags anzeigen. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Kein aktiver **Pull Contacts** Auftrag gefunden. Starten Sie einen mit `contacts pull`. + cli.cmd.server.description: |- Server verwalten. @@ -189,6 +251,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Globale Optionen:** +task.contacts.status: |- + 👤 Kontakte abrufen: **{{state}}** + - ✅ Erfolgreich: {{{progress.ok}}} Kontakte + - ⚪ Übersprungen: {{{progress.skipped}}} Kontakte + - ⚠️ Fehler: {{{progress.errors}}} Kontakte + + {{#job}} + ⌛ Zeitplan: + - Eingereiht: {{{timestamp}}} + {{#processedOn}} + - Start: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Ende: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Details: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Kontakte werden abgerufen... + +task.contacts.progress: |- + Kontakte abrufen: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Senden Sie **help**, um die Liste der verfügbaren Befehle zu erhalten. diff --git a/src/apps/chatwoot/i18n/locales/en-US.yaml b/src/apps/chatwoot/i18n/locales/en-US.yaml index ffcbc39e..2709e1aa 100644 --- a/src/apps/chatwoot/i18n/locales/en-US.yaml +++ b/src/apps/chatwoot/i18n/locales/en-US.yaml @@ -290,6 +290,9 @@ app.updated.message: |- ✅ Settings for WhatsApp session **{{{name}}}** have been updated. +cli.usage.arguments: |- + [action] + cli.usage.command: |- [command] @@ -311,6 +314,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Session**: +cli.cmd.root.sub.sync: |- + 🔄️ **Sync**: + cli.cmd.session.status.description: |- Get current session status. @@ -332,6 +338,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Capture a session screenshot (available only with the **WEBJS** engine). +cli.cmd.contacts.summary: |- + Pull WhatsApp contacts. + +cli.cmd.contacts.description: |- + 👤 Run `contacts pull` to launch a sync or `contacts status` to inspect progress. + +cli.cmd.contacts.action.description: |- + `start` to launch a contact pull or `status` to inspect the active job. + +cli.cmd.contacts.pull.description: |- + Start pulling WhatsApp contacts into ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Number of contacts to pull in each batch. + +cli.cmd.contacts.pull.option.delay-contact: |- + Delay between syncing each contact to avoid rate limiting in pulling avatars. + +cli.cmd.contacts.pull.option.delay-batch: |- + Delay between batches of contacts. + +cli.cmd.contacts.pull.option.avatar: |- + Choose how to pull avatars for contacts: `skip`, `if-missing`, or `update`. + +cli.cmd.contacts.pull.option.groups: |- + Include WhatsApp group contacts in the sync. + +cli.cmd.contacts.pull.option.no-lids: |- + Skip WhatsApp LID contacts (linked-device JIDs) in the sync. + +cli.cmd.contacts.pull.option.no-attributes: |- + Do not pull contact attributes if contact already exists (it speeds up the process). + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** job is already running. Wait for it to finish or stop it before starting a new one. + + Send `contacts status` for details. + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** job queued. + + We'll share a progress update every **{{batch}}** contacts. + Pulling contacts may take a while - check the latest status anytime with `contacts status`. + +cli.cmd.options.job.attempts: |- + Number of times to retry the job before marking it as failed. + +cli.cmd.options.job.timeout: |- + Maximum time the job is allowed to run before it is retried (supports values like `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Show the status of the current pull job. + +cli.cmd.contacts.status.not-found: |- + ℹ️ No active **Pull Contacts** job found. Start one with `contacts pull`. + cli.cmd.server.description: |- Manage the server. @@ -345,7 +407,7 @@ cli.cmd.server.reboot.option.force: |- Force an immediate reboot. cli.help.usage.title: |- - Usage: send a command to the conversation in the format: + Send a command to the conversation in the format: cli.help.commands.defaultGroup: |- **Commands:** @@ -359,6 +421,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Global Options:** +task.contacts.status: |- + 👤 Pull Contacts: **{{state}}** + - ✅ Successful: {{{progress.ok}}} contacts + - ⚪ Skipped: {{{progress.skipped}}} contacts + - ⚠️ Errors: {{{progress.errors}}} contacts + + {{#job}} + ⌛ Timeline: + - Queued: {{{timestamp}}} + {{#processedOn}} + - Start At: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Finished At: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Details: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Pulling contacts... + +task.contacts.progress: |- + Pull Contacts: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Send **help** to get the list of available commands. diff --git a/src/apps/chatwoot/i18n/locales/es-ES.yaml b/src/apps/chatwoot/i18n/locales/es-ES.yaml index 79acf5a3..eac163b8 100644 --- a/src/apps/chatwoot/i18n/locales/es-ES.yaml +++ b/src/apps/chatwoot/i18n/locales/es-ES.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ La configuración para la sesión de WhatsApp **{{{name}}}** ha sido actualizada. +cli.usage.arguments: |- + [acción] + cli.usage.command: |- [comando] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Servidor**: +cli.cmd.root.sub.sync: |- + 🔄️ **Sincronización**: + cli.cmd.session.status.description: |- Obtén el estado actual de la sesión. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Captura una imagen de la sesión (disponible solo con el motor **WEBJS**). +cli.cmd.contacts.summary: |- + Obtener contactos de WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Ejecuta `contacts pull` para iniciar una sincronización o `contacts status` para revisar el progreso. + +cli.cmd.contacts.action.description: |- + Usa `start` para comenzar la extracción de contactos o `status` para revisar el trabajo activo. + +cli.cmd.contacts.pull.description: |- + Empieza a importar contactos de WhatsApp en ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Cantidad de contactos a extraer en cada lote. + +cli.cmd.contacts.pull.option.delay-contact: |- + Retraso entre cada contacto para evitar límites al obtener los avatares. + +cli.cmd.contacts.pull.option.delay-batch: |- + Retraso entre los lotes de contactos. + +cli.cmd.contacts.pull.option.avatar: |- + Elige cómo obtener los avatares de los contactos: `skip`, `if-missing` o `update`. + +cli.cmd.contacts.pull.option.groups: |- + Incluir contactos de grupos de WhatsApp en la sincronización. + +cli.cmd.contacts.pull.option.no-lids: |- + Omitir contactos LID de WhatsApp (JID de dispositivo vinculado) en la sincronización. + +cli.cmd.contacts.pull.option.no-attributes: |- + No obtengas los atributos del contacto si ya existe (acelera el proceso). + +cli.cmd.contacts.pull.already-running: |- + ⏳ El trabajo **Pull Contacts** ya se está ejecutando. Espera a que finalice o deténlo antes de iniciar uno nuevo. + + Envía `contacts status` para más detalles. + +cli.cmd.contacts.pull.queued: |- + 👥 Trabajo **Pull Contacts** en cola. + + Compartiremos una actualización de progreso cada **{{batch}}** contactos. + La importación puede tardar; consulta el estado más reciente en cualquier momento con `contacts status`. + +cli.cmd.options.job.attempts: |- + Número de intentos antes de marcar el trabajo como fallido. + +cli.cmd.options.job.timeout: |- + Tiempo máximo que puede ejecutarse el trabajo antes de reintentarlo (admite valores como `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Mostrar el estado del trabajo de importación actual. + +cli.cmd.contacts.status.not-found: |- + ℹ️ No se encontró ningún trabajo activo de **Pull Contacts**. Inicia uno con `contacts pull`. + cli.cmd.server.description: |- Administra el servidor. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Opciones globales:** +task.contacts.status: |- + 👤 Extracción de contactos: **{{state}}** + - ✅ Correctos: {{{progress.ok}}} contactos + - ⚪ Omitidos: {{{progress.skipped}}} contactos + - ⚠️ Errores: {{{progress.errors}}} contactos + + {{#job}} + ⌛ Cronología: + - En cola: {{{timestamp}}} + {{#processedOn}} + - Inicio: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Finalizó: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Detalles: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Extrayendo contactos... + +task.contacts.progress: |- + Extracción de contactos: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Envía **help** para obtener la lista de comandos disponibles. diff --git a/src/apps/chatwoot/i18n/locales/fa-IR.yaml b/src/apps/chatwoot/i18n/locales/fa-IR.yaml index aeb681e4..fab08393 100644 --- a/src/apps/chatwoot/i18n/locales/fa-IR.yaml +++ b/src/apps/chatwoot/i18n/locales/fa-IR.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ تنظیمات جلسه واتساپ **{{{name}}}** به‌روزرسانی شده است. +cli.usage.arguments: |- + [اقدام] + cli.usage.command: |- [دستور] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **سرور**: +cli.cmd.root.sub.sync: |- + 🔄️ **همگام‌سازی**: + cli.cmd.session.status.description: |- وضعیت فعلی جلسه را دریافت کنید. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- از جلسه اسکرین‌شات بگیرید (فقط با موتور **WEBJS** در دسترس است). +cli.cmd.contacts.summary: |- + دریافت مخاطبان واتساپ. + +cli.cmd.contacts.description: |- + 👤 `contacts pull` را اجرا کنید تا همگام‌سازی آغاز شود یا `contacts status` را برای بررسی پیشرفت اجرا کنید. + +cli.cmd.contacts.action.description: |- + از `start` برای آغاز دریافت مخاطبان یا از `status` برای بررسی کار فعال استفاده کنید. + +cli.cmd.contacts.pull.description: |- + دریافت مخاطبان واتساپ در ChatWoot را آغاز کنید. + +cli.cmd.contacts.pull.option.batch: |- + تعداد مخاطبانی که در هر دسته دریافت می‌شوند. + +cli.cmd.contacts.pull.option.delay-contact: |- + تأخیر بین همگام‌سازی هر مخاطب برای جلوگیری از محدودیت نرخ هنگام دریافت آواتار. + +cli.cmd.contacts.pull.option.delay-batch: |- + تأخیر بین دسته‌های مخاطبان. + +cli.cmd.contacts.pull.option.avatar: |- + نحوه دریافت آواتار مخاطبان را انتخاب کنید: `skip`، `if-missing` یا `update`. + +cli.cmd.contacts.pull.option.groups: |- + مخاطبان گروه‌های واتساپ را در همگام‌سازی قرار دهید. + +cli.cmd.contacts.pull.option.no-lids: |- + مخاطبان LID واتساپ (شناسه‌های دستگاه متصل JID) را در همگام‌سازی نادیده بگیرید. + +cli.cmd.contacts.pull.option.no-attributes: |- + اگر مخاطب از قبل وجود دارد ویژگی‌های او را دریافت نکنید (فرآیند را سریع‌تر می‌کند). + +cli.cmd.contacts.pull.already-running: |- + ⏳ کار **Pull Contacts** هم‌اکنون در حال اجراست. صبر کنید تا تمام شود یا قبل از شروع کار جدید آن را متوقف کنید. + + برای جزئیات `contacts status` را ارسال کنید. + +cli.cmd.contacts.pull.queued: |- + 👥 کار **Pull Contacts** در صف قرار گرفت. + + پس از هر **{{batch}}** مخاطب یک به‌روزرسانی پیشرفت ارسال می‌کنیم. + دریافت مخاطبان ممکن است زمان‌بر باشد؛ هر زمان با `contacts status` آخرین وضعیت را بررسی کنید. + +cli.cmd.options.job.attempts: |- + تعداد دفعات تلاش مجدد پیش از علامت‌گذاری کار به‌عنوان ناموفق. + +cli.cmd.options.job.timeout: |- + حداکثر مدت اجرای کار پیش از تلاش دوباره (از مقادیری مانند `30s`، `5m`، `1h` پشتیبانی می‌کند). + +cli.cmd.contacts.status.description: |- + وضعیت کار دریافت فعلی را نمایش دهید. + +cli.cmd.contacts.status.not-found: |- + ℹ️ هیچ کار فعال **Pull Contacts** یافت نشد. با `contacts pull` یکی آغاز کنید. + cli.cmd.server.description: |- سرور را مدیریت کنید. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **گزینه‌های عمومی:** +task.contacts.status: |- + 👤 دریافت مخاطبان: **{{state}}** + - ✅ موفق: {{{progress.ok}}} مخاطب + - ⚪ نادیده‌گرفته‌شده: {{{progress.skipped}}} مخاطب + - ⚠️ خطا: {{{progress.errors}}} مخاطب + + {{#job}} + ⌛ جدول زمانی: + - در صف: {{{timestamp}}} + {{#processedOn}} + - زمان شروع: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - زمان پایان: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + جزئیات: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + در حال دریافت مخاطبان... + +task.contacts.progress: |- + دریافت مخاطبان: ✅ {{progress.ok}}، ⚪ {{progress.skipped}}، ⚠️ {{progress.errors}} + app.help.reminder: |- برای دریافت لیست دستورات موجود، **help** را ارسال کنید. diff --git a/src/apps/chatwoot/i18n/locales/fr-FR.yaml b/src/apps/chatwoot/i18n/locales/fr-FR.yaml index f16f2243..2887375b 100644 --- a/src/apps/chatwoot/i18n/locales/fr-FR.yaml +++ b/src/apps/chatwoot/i18n/locales/fr-FR.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ Les paramètres de la session WhatsApp **{{{name}}}** ont été mis à jour. +cli.usage.arguments: |- + [action] + cli.usage.command: |- [commande] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Serveur** : +cli.cmd.root.sub.sync: |- + 🔄️ **Synchronisation** : + cli.cmd.session.status.description: |- Obtenez l'état actuel de la session. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Prenez une capture d'écran de la session (disponible uniquement avec le moteur **WEBJS**). +cli.cmd.contacts.summary: |- + Importer les contacts WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Exécutez `contacts pull` pour lancer une synchronisation ou `contacts status` pour suivre la progression. + +cli.cmd.contacts.action.description: |- + Utilisez `start` pour démarrer l'import de contacts ou `status` pour consulter la tâche active. + +cli.cmd.contacts.pull.description: |- + Commencez à importer les contacts WhatsApp dans ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Nombre de contacts importés par lot. + +cli.cmd.contacts.pull.option.delay-contact: |- + Délai entre chaque contact pour éviter les limites lors du chargement des avatars. + +cli.cmd.contacts.pull.option.delay-batch: |- + Délai entre les lots de contacts. + +cli.cmd.contacts.pull.option.avatar: |- + Choisissez comment charger les avatars des contacts : `skip`, `if-missing` ou `update`. + +cli.cmd.contacts.pull.option.groups: |- + Inclure les contacts de groupes WhatsApp dans la synchronisation. + +cli.cmd.contacts.pull.option.no-lids: |- + Exclure les contacts LID WhatsApp (JID d'appareil lié) de la synchronisation. + +cli.cmd.contacts.pull.option.no-attributes: |- + Ne pas importer les attributs si le contact existe déjà (accélère le processus). + +cli.cmd.contacts.pull.already-running: |- + ⏳ La tâche **Pull Contacts** est déjà en cours. Attendez qu'elle se termine ou arrêtez-la avant d'en démarrer une nouvelle. + + Envoyez `contacts status` pour plus de détails. + +cli.cmd.contacts.pull.queued: |- + 👥 Tâche **Pull Contacts** mise en file d'attente. + + Nous partagerons une mise à jour toutes les **{{batch}}** contacts. + L'import peut prendre du temps — consultez l'état le plus récent à tout moment avec `contacts status`. + +cli.cmd.options.job.attempts: |- + Nombre de tentatives avant de marquer la tâche comme échouée. + +cli.cmd.options.job.timeout: |- + Durée maximale autorisée avant une nouvelle tentative (accepte `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Afficher l'état de la tâche d'import en cours. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Aucune tâche **Pull Contacts** active trouvée. Démarrez-en une avec `contacts pull`. + cli.cmd.server.description: |- Gérez le serveur. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Options globales :** +task.contacts.status: |- + 👤 Import de contacts : **{{state}}** + - ✅ Réussis : {{{progress.ok}}} contacts + - ⚪ Ignorés : {{{progress.skipped}}} contacts + - ⚠️ Erreurs : {{{progress.errors}}} contacts + + {{#job}} + ⌛ Chronologie : + - En file d'attente : {{{timestamp}}} + {{#processedOn}} + - Début : {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Fin : {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Détails : + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Importation des contacts... + +task.contacts.progress: |- + Import de contacts : ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Envoyez **help** pour obtenir la liste des commandes disponibles. diff --git a/src/apps/chatwoot/i18n/locales/he-IL.yaml b/src/apps/chatwoot/i18n/locales/he-IL.yaml index a04fb092..1665ff4e 100644 --- a/src/apps/chatwoot/i18n/locales/he-IL.yaml +++ b/src/apps/chatwoot/i18n/locales/he-IL.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ ההגדרות עבור הפעלת וואטסאפ **{{{name}}}** עודכנו. +cli.usage.arguments: |- + [פעולה] + cli.usage.command: |- [פקודה] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **שרת**: +cli.cmd.root.sub.sync: |- + 🔄️ **סנכרון**: + cli.cmd.session.status.description: |- קבל את סטטוס ההפעלה הנוכחי. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- צלם צילום מסך של ההפעלה (זמין רק עם מנוע **WEBJS**). +cli.cmd.contacts.summary: |- + משיכת אנשי קשר של וואטסאפ. + +cli.cmd.contacts.description: |- + 👤 הרץ `contacts pull` כדי להתחיל סנכרון או `contacts status` כדי לבדוק את ההתקדמות. + +cli.cmd.contacts.action.description: |- + השתמש ב-`start` כדי להתחיל למשוך אנשי קשר או ב-`status` כדי לבדוק את המשימה הפעילה. + +cli.cmd.contacts.pull.description: |- + התחל למשוך אנשי קשר של וואטסאפ אל ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + מספר אנשי הקשר שנמשכים בכל אצווה. + +cli.cmd.contacts.pull.option.delay-contact: |- + השהיה בין כל איש קשר כדי להימנע ממגבלות בעת משיכת תמונות הפרופיל. + +cli.cmd.contacts.pull.option.delay-batch: |- + השהיה בין אצוות של אנשי קשר. + +cli.cmd.contacts.pull.option.avatar: |- + בחר כיצד למשוך תמונות פרופיל: `skip`, `if-missing` או `update`. + +cli.cmd.contacts.pull.option.groups: |- + כלול בסנכרון אנשי קשר של קבוצות WhatsApp. + +cli.cmd.contacts.pull.option.no-lids: |- + דלג בסנכרון על אנשי קשר מסוג LID של WhatsApp (כתובות JID של מכשירים מקושרים). + +cli.cmd.contacts.pull.option.no-attributes: |- + אל תמשוך מאפייני איש קשר אם הוא כבר קיים (מאיץ את התהליך). + +cli.cmd.contacts.pull.already-running: |- + ⏳ המשימה **Pull Contacts** כבר פועלת. המתן שתסתיים או עצור אותה לפני שמתחילים חדשה. + + שלח `contacts status` לקבלת פרטים. + +cli.cmd.contacts.pull.queued: |- + 👥 המשימה **Pull Contacts** בתור. + + נעדכן בהתקדמות כל **{{batch}}** אנשי קשר. + משיכת אנשי קשר יכולה לקחת זמן – בדוק בכל עת את הסטטוס האחרון עם `contacts status`. + +cli.cmd.options.job.attempts: |- + מספר ניסיונות החזרה לפני שסימנים את המשימה ככושלת. + +cli.cmd.options.job.timeout: |- + משך הזמן המרבי שבו המשימה יכולה לפעול לפני ניסיון נוסף (תומך בערכים כגון `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + הצגת הסטטוס של משימת המשיכה הנוכחית. + +cli.cmd.contacts.status.not-found: |- + ℹ️ לא נמצאה משימת **Pull Contacts** פעילה. התחל אחת עם `contacts pull`. + cli.cmd.server.description: |- נהל את השרת. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **אפשרויות גלובליות:** +task.contacts.status: |- + 👤 משיכת אנשי קשר: **{{state}}** + - ✅ הצלחות: {{{progress.ok}}} אנשי קשר + - ⚪ שדלגו עליהם: {{{progress.skipped}}} אנשי קשר + - ⚠️ שגיאות: {{{progress.errors}}} אנשי קשר + + {{#job}} + ⌛ ציר זמן: + - נכנסה לתור: {{{timestamp}}} + {{#processedOn}} + - התחלה: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - סיום: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + פרטים: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + אנשי קשר נמשכים... + +task.contacts.progress: |- + משיכת אנשי קשר: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- שלח **help** כדי לקבל את רשימת הפקודות הזמינות. diff --git a/src/apps/chatwoot/i18n/locales/hi-IN.yaml b/src/apps/chatwoot/i18n/locales/hi-IN.yaml index efa9929e..d8bce8d8 100644 --- a/src/apps/chatwoot/i18n/locales/hi-IN.yaml +++ b/src/apps/chatwoot/i18n/locales/hi-IN.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ व्हाट्सएप सत्र **{{{name}}}** की सेटिंग्स अपडेट की गई हैं। +cli.usage.arguments: |- + [क्रिया] + cli.usage.command: |- [कमांड] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **सर्वर**: +cli.cmd.root.sub.sync: |- + 🔄️ **सिंक**: + cli.cmd.session.status.description: |- वर्तमान सत्र की स्थिति प्राप्त करें। @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- सत्र का स्क्रीनशॉट लें (केवल **WEBJS** इंजन के साथ उपलब्ध)। +cli.cmd.contacts.summary: |- + व्हाट्सएप संपर्क प्राप्त करें। + +cli.cmd.contacts.description: |- + 👤 सिंक शुरू करने के लिए `contacts pull` चलाएँ या प्रगति देखने के लिए `contacts status` चलाएँ। + +cli.cmd.contacts.action.description: |- + `start` से संपर्क खींचना शुरू करें या `status` से सक्रिय कार्य जांचें। + +cli.cmd.contacts.pull.description: |- + व्हाट्सएप संपर्कों को ChatWoot में लाना शुरू करें। + +cli.cmd.contacts.pull.option.batch: |- + प्रत्येक बैच में खींचे जाने वाले संपर्कों की संख्या। + +cli.cmd.contacts.pull.option.delay-contact: |- + हर संपर्क के बीच देरी ताकि अवतार खींचते समय रेट लिमिट से बचा जा सके। + +cli.cmd.contacts.pull.option.delay-batch: |- + संपर्क बैचों के बीच देरी। + +cli.cmd.contacts.pull.option.avatar: |- + संपर्क अवतार कैसे खींचें चुनें: `skip`, `if-missing`, या `update`। + +cli.cmd.contacts.pull.option.groups: |- + सिंक में WhatsApp समूह संपर्क शामिल करें। + +cli.cmd.contacts.pull.option.no-lids: |- + सिंक में WhatsApp LID संपर्क (linked-device JID) को छोड़ें। + +cli.cmd.contacts.pull.option.no-attributes: |- + यदि संपर्क पहले से मौजूद है तो उसके गुण न खींचें (इससे प्रक्रिया तेज होती है)। + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** कार्य पहले से ही चल रहा है। इसे समाप्त होने दें या नया शुरू करने से पहले रोकें। + + विवरण के लिए `contacts status` भेजें। + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** कार्य कतार में जोड़ा गया। + + हम हर **{{batch}}** संपर्क पर प्रगति अपडेट साझा करेंगे। + संपर्क खींचने में समय लग सकता है — किसी भी समय `contacts status` से नवीनतम स्थिति देखें। + +cli.cmd.options.job.attempts: |- + कार्य को विफल मानने से पहले पुनः प्रयासों की संख्या। + +cli.cmd.options.job.timeout: |- + कार्य को पुनः कोशिश करने से पहले अधिकतम चलने का समय (जैसे `30s`, `5m`, `1h`)। + +cli.cmd.contacts.status.description: |- + वर्तमान खींचने वाले कार्य की स्थिति दिखाएँ। + +cli.cmd.contacts.status.not-found: |- + ℹ️ कोई सक्रिय **Pull Contacts** कार्य नहीं मिला। `contacts pull` से एक शुरू करें। + cli.cmd.server.description: |- सर्वर का प्रबंधन करें। @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **ग्लोबल विकल्प:** +task.contacts.status: |- + 👤 संपर्क खींचना: **{{state}}** + - ✅ सफल: {{{progress.ok}}} संपर्क + - ⚪ छोड़े गए: {{{progress.skipped}}} संपर्क + - ⚠️ त्रुटियाँ: {{{progress.errors}}} संपर्क + + {{#job}} + ⌛ समयरेखा: + - कतार में: {{{timestamp}}} + {{#processedOn}} + - शुरू हुआ: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - समाप्त: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + विवरण: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + संपर्क खींचे जा रहे हैं... + +task.contacts.progress: |- + संपर्क खींचना: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- उपलब्ध कमांड की सूची प्राप्त करने के लिए **help** भेजें। diff --git a/src/apps/chatwoot/i18n/locales/id-ID.yaml b/src/apps/chatwoot/i18n/locales/id-ID.yaml index f4049271..9b191c6a 100644 --- a/src/apps/chatwoot/i18n/locales/id-ID.yaml +++ b/src/apps/chatwoot/i18n/locales/id-ID.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ Pengaturan untuk sesi WhatsApp **{{{name}}}** telah diperbarui. +cli.usage.arguments: |- + [aksi] + cli.usage.command: |- [perintah] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Server**: +cli.cmd.root.sub.sync: |- + 🔄️ **Sinkronisasi**: + cli.cmd.session.status.description: |- Dapatkan status sesi saat ini. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Ambil tangkapan layar sesi (hanya tersedia dengan mesin **WEBJS**). +cli.cmd.contacts.summary: |- + Tarik kontak WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Jalankan `contacts pull` untuk memulai sinkronisasi atau `contacts status` untuk melihat progres. + +cli.cmd.contacts.action.description: |- + Gunakan `start` untuk mulai menarik kontak atau `status` untuk memeriksa pekerjaan aktif. + +cli.cmd.contacts.pull.description: |- + Mulai menarik kontak WhatsApp ke ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Jumlah kontak yang ditarik di setiap batch. + +cli.cmd.contacts.pull.option.delay-contact: |- + Jeda antar tiap kontak untuk menghindari pembatasan saat mengambil avatar. + +cli.cmd.contacts.pull.option.delay-batch: |- + Jeda antar batch kontak. + +cli.cmd.contacts.pull.option.avatar: |- + Pilih cara mengambil avatar kontak: `skip`, `if-missing`, atau `update`. + +cli.cmd.contacts.pull.option.groups: |- + Sertakan kontak grup WhatsApp dalam sinkronisasi. + +cli.cmd.contacts.pull.option.no-lids: |- + Lewati kontak LID WhatsApp (JID perangkat terhubung) saat sinkronisasi. + +cli.cmd.contacts.pull.option.no-attributes: |- + Jangan ambil atribut kontak jika kontak sudah ada (mempercepat proses). + +cli.cmd.contacts.pull.already-running: |- + ⏳ Pekerjaan **Pull Contacts** sudah berjalan. Tunggu hingga selesai atau hentikan sebelum memulai yang baru. + + Kirim `contacts status` untuk detail. + +cli.cmd.contacts.pull.queued: |- + 👥 Pekerjaan **Pull Contacts** masuk antrean. + + Kami akan membagikan pembaruan progres setiap **{{batch}}** kontak. + Penarikan kontak bisa memakan waktu—periksa status terbaru kapan saja dengan `contacts status`. + +cli.cmd.options.job.attempts: |- + Jumlah percobaan ulang sebelum pekerjaan dianggap gagal. + +cli.cmd.options.job.timeout: |- + Durasi maksimum pekerjaan sebelum dicoba ulang (mendukung nilai seperti `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Tampilkan status pekerjaan penarikan saat ini. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Tidak ada pekerjaan **Pull Contacts** aktif. Mulai satu dengan `contacts pull`. + cli.cmd.server.description: |- Kelola server. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Opsi global:** +task.contacts.status: |- + 👤 Penarikan kontak: **{{state}}** + - ✅ Berhasil: {{{progress.ok}}} kontak + - ⚪ Dilewati: {{{progress.skipped}}} kontak + - ⚠️ Kesalahan: {{{progress.errors}}} kontak + + {{#job}} + ⌛ Linimasa: + - Masuk antrean: {{{timestamp}}} + {{#processedOn}} + - Mulai: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Selesai: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Detail: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Menarik kontak... + +task.contacts.progress: |- + Penarikan kontak: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Kirim **help** untuk mendapatkan daftar perintah yang tersedia. diff --git a/src/apps/chatwoot/i18n/locales/pa-PK.yaml b/src/apps/chatwoot/i18n/locales/pa-PK.yaml index 8eea90b4..53d518cd 100644 --- a/src/apps/chatwoot/i18n/locales/pa-PK.yaml +++ b/src/apps/chatwoot/i18n/locales/pa-PK.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ ਵਟਸਐਪ ਸੈਸ਼ਨ **{{{name}}}** ਲਈ ਸੈਟਿੰਗਾਂ ਅਪਡੇਟ ਕੀਤੀਆਂ ਗਈਆਂ ਹਨ। +cli.usage.arguments: |- + [ਕਾਰਵਾਈ] + cli.usage.command: |- [ਕਮਾਂਡ] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **ਸਰਵਰ**: +cli.cmd.root.sub.sync: |- + 🔄️ **ਸਿੰਕ**: + cli.cmd.session.status.description: |- ਮੌਜੂਦਾ ਸੈਸ਼ਨ ਦੀ ਸਥਿਤੀ ਪ੍ਰਾਪਤ ਕਰੋ। @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- ਸੈਸ਼ਨ ਦਾ ਸਕ੍ਰੀਨਸ਼ਾਟ ਲਓ (ਸਿਰਫ਼ **WEBJS** ਇੰਜਨ ਨਾਲ ਉਪਲਬਧ)। +cli.cmd.contacts.summary: |- + ਵਟਸਐਪ ਸੰਪਰਕ ਖਿੱਚੋ। + +cli.cmd.contacts.description: |- + 👤 ਸਿੰਕ ਸ਼ੁਰੂ ਕਰਨ ਲਈ `contacts pull` ਚਲਾਓ ਜਾਂ ਤਰੱਕੀ ਦੇਖਣ ਲਈ `contacts status` ਚਲਾਓ। + +cli.cmd.contacts.action.description: |- + `start` ਨਾਲ ਸੰਪਰਕ ਖਿੱਚਣਾ ਸ਼ੁਰੂ ਕਰੋ ਜਾਂ `status` ਨਾਲ ਸਰਗਰਮ ਕੰਮ ਦੀ ਜਾਂਚ ਕਰੋ। + +cli.cmd.contacts.pull.description: |- + ਵਟਸਐਪ ਸੰਪਰਕਾਂ ਨੂੰ ChatWoot ਵਿੱਚ ਲਿਆਉਣਾ ਸ਼ੁਰੂ ਕਰੋ। + +cli.cmd.contacts.pull.option.batch: |- + ਹਰ ਬੈਚ ਵਿੱਚ ਖਿੱਚੇ ਜਾਣ ਵਾਲੇ ਸੰਪਰਕਾਂ ਦੀ ਗਿਣਤੀ। + +cli.cmd.contacts.pull.option.delay-contact: |- + ਹਰ ਸੰਪਰਕ ਦੇ ਵਿਚਕਾਰ ਦੇਰੀ, ਤਾਂ ਜੋ ਅਵਤਾਰ ਖਿੱਚਦੇ ਸਮੇਂ ਰੇਟ ਲਿਮਟ ਤੋਂ ਬਚਿਆ ਜਾ ਸਕੇ। + +cli.cmd.contacts.pull.option.delay-batch: |- + ਸੰਪਰਕ ਬੈਚਾਂ ਦੇ ਵਿਚਕਾਰ ਦੇਰੀ। + +cli.cmd.contacts.pull.option.avatar: |- + ਚੁਣੋ ਕਿ ਅਵਤਾਰ ਕਿਵੇਂ ਖਿੱਚਣੇ ਹਨ: `skip`, `if-missing`, ਜਾਂ `update`। + +cli.cmd.contacts.pull.option.groups: |- + ਸਿੰਕ ਵਿੱਚ WhatsApp ਗਰੁੱਪ ਸੰਪਰਕ ਸ਼ਾਮਲ ਕਰੋ। + +cli.cmd.contacts.pull.option.no-lids: |- + ਸਿੰਕ ਵਿੱਚ WhatsApp LID ਸੰਪਰਕ (ਲਿੰਕਡ-ਡਿਵਾਈਸ JID) ਨੂੰ ਛੱਡੋ। + +cli.cmd.contacts.pull.option.no-attributes: |- + ਜੇ ਸੰਪਰਕ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ ਤਾਂ ਉਸਦੇ ਗੁਣ ਨਾ ਖਿੱਚੋ (ਇਸ ਨਾਲ ਪ੍ਰਕਿਰਿਆ ਤੇਜ਼ ਹੁੰਦੀ ਹੈ)। + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** ਕੰਮ ਪਹਿਲਾਂ ਹੀ ਚੱਲ ਰਿਹਾ ਹੈ। ਇਸਦੇ ਮੁਕੰਮਲ ਹੋਣ ਦੀ ਉਡੀਕ ਕਰੋ ਜਾਂ ਨਵਾਂ ਸ਼ੁਰੂ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਇਸਨੂੰ ਰੋਕੋ। + + ਵੇਰਵਿਆਂ ਲਈ `contacts status` ਭੇਜੋ। + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** ਕੰਮ ਕਤਾਰ ਵਿੱਚ ਲਗਾਇਆ ਗਿਆ ਹੈ। + + ਅਸੀਂ ਹਰ **{{batch}}** ਸੰਪਰਕ ਤੋਂ ਬਾਅਦ ਤਰੱਕੀ ਅਪਡੇਟ ਸਾਂਝੇ ਕਰਾਂਗੇ। + ਸੰਪਰਕ ਖਿੱਚਣ ਵਿੱਚ ਸਮਾਂ ਲੱਗ ਸਕਦਾ ਹੈ — ਕਿਸੇ ਵੀ ਵੇਲੇ `contacts status` ਨਾਲ ਤਾਜ਼ਾ ਸਥਿਤੀ ਜਾਂਚੋ। + +cli.cmd.options.job.attempts: |- + ਕੰਮ ਨੂੰ ਅਸਫਲ ਘੋਸ਼ਿਤ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ਾਂ ਦੀ ਗਿਣਤੀ। + +cli.cmd.options.job.timeout: |- + ਕੰਮ ਨੂੰ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ ਚੱਲਣ ਦਾ ਵੱਧ ਤੋਂ ਵੱਧ ਸਮਾਂ (ਜਿਵੇਂ `30s`, `5m`, `1h`)। + +cli.cmd.contacts.status.description: |- + ਮੌਜੂਦਾ ਖਿੱਚਣ ਵਾਲੇ ਕੰਮ ਦੀ ਸਥਿਤੀ ਵੇਖਾਓ। + +cli.cmd.contacts.status.not-found: |- + ℹ️ ਕੋਈ ਸਰਗਰਮ **Pull Contacts** ਕੰਮ ਨਹੀਂ ਮਿਲਿਆ। `contacts pull` ਨਾਲ ਇੱਕ ਸ਼ੁਰੂ ਕਰੋ। + cli.cmd.server.description: |- ਸਰਵਰ ਦਾ ਪ੍ਰਬੰਧ ਕਰੋ। @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **ਗਲੋਬਲ ਚੋਣਾਂ:** +task.contacts.status: |- + 👤 ਸੰਪਰਕ ਖਿੱਚਣਾ: **{{state}}** + - ✅ ਸਫਲ: {{{progress.ok}}} ਸੰਪਰਕ + - ⚪ ਛੱਡੇ ਗਏ: {{{progress.skipped}}} ਸੰਪਰਕ + - ⚠️ ਗਲਤੀਆਂ: {{{progress.errors}}} ਸੰਪਰਕ + + {{#job}} + ⌛ ਸਮਾਂ-ਰੇਖਾ: + - ਕਤਾਰ ਵਿੱਚ: {{{timestamp}}} + {{#processedOn}} + - ਸ਼ੁਰੂ: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - ਸਮਾਪਤ: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + ਵੇਰਵੇ: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + ਸੰਪਰਕ ਖਿੱਚੇ ਜਾ ਰਹੇ ਹਨ... + +task.contacts.progress: |- + ਸੰਪਰਕ ਖਿੱਚਣਾ: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- ਉਪਲਬਧ ਕਮਾਂਡਾਂ ਦੀ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ **help** ਭੇਜੋ। diff --git a/src/apps/chatwoot/i18n/locales/pt-BR.yaml b/src/apps/chatwoot/i18n/locales/pt-BR.yaml index 8b95be92..cef5df3e 100644 --- a/src/apps/chatwoot/i18n/locales/pt-BR.yaml +++ b/src/apps/chatwoot/i18n/locales/pt-BR.yaml @@ -120,6 +120,9 @@ app.updated.message: |- ✅ As configurações para a sessão do WhatsApp **{{{name}}}** foram atualizadas. +cli.usage.arguments: |- + [ação] + cli.usage.command: |- [comando] @@ -141,6 +144,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Servidor**: +cli.cmd.root.sub.sync: |- + 🔄️ **Sincronização**: + cli.cmd.session.status.description: |- Obtenha o status atual da sessão. @@ -162,6 +168,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Faça uma captura de tela da sessão (disponível apenas com o mecanismo **WEBJS**). +cli.cmd.contacts.summary: |- + Buscar contatos do WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Execute `contacts pull` para iniciar uma sincronização ou `contacts status` para acompanhar o progresso. + +cli.cmd.contacts.action.description: |- + Use `start` para iniciar a busca de contatos ou `status` para conferir o trabalho ativo. + +cli.cmd.contacts.pull.description: |- + Comece a importar contatos do WhatsApp para o ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Quantidade de contatos buscados em cada lote. + +cli.cmd.contacts.pull.option.delay-contact: |- + Atraso entre cada contato para evitar limites ao buscar avatares. + +cli.cmd.contacts.pull.option.delay-batch: |- + Atraso entre os lotes de contatos. + +cli.cmd.contacts.pull.option.avatar: |- + Escolha como buscar os avatares dos contatos: `skip`, `if-missing` ou `update`. + +cli.cmd.contacts.pull.option.groups: |- + Incluir contatos de grupo do WhatsApp na sincronização. + +cli.cmd.contacts.pull.option.no-lids: |- + Ignorar contatos LID do WhatsApp (JID de dispositivo vinculado) na sincronização. + +cli.cmd.contacts.pull.option.no-attributes: |- + Não busque atributos do contato se ele já existir (acelera o processo). + +cli.cmd.contacts.pull.already-running: |- + ⏳ O trabalho **Pull Contacts** já está em execução. Aguarde terminar ou interrompa-o antes de iniciar um novo. + + Envie `contacts status` para obter detalhes. + +cli.cmd.contacts.pull.queued: |- + 👥 Trabalho **Pull Contacts** enfileirado. + + Compartilharemos atualizações a cada **{{batch}}** contatos. + Buscar contatos pode levar algum tempo — verifique o status mais recente a qualquer momento com `contacts status`. + +cli.cmd.options.job.attempts: |- + Número de tentativas antes de marcar o trabalho como falho. + +cli.cmd.options.job.timeout: |- + Tempo máximo permitido antes de tentar novamente (aceita valores como `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Mostrar o status do trabalho de busca atual. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Nenhum trabalho ativo **Pull Contacts** encontrado. Inicie um com `contacts pull`. + cli.cmd.server.description: |- Gerencie o servidor. @@ -189,6 +251,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Opções globais:** +task.contacts.status: |- + 👤 Busca de contatos: **{{state}}** + - ✅ Sucesso: {{{progress.ok}}} contatos + - ⚪ Ignorados: {{{progress.skipped}}} contatos + - ⚠️ Erros: {{{progress.errors}}} contatos + + {{#job}} + ⌛ Linha do tempo: + - Na fila: {{{timestamp}}} + {{#processedOn}} + - Início: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Fim: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Detalhes: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Buscando contatos... + +task.contacts.progress: |- + Busca de contatos: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Envie **help** para obter a lista de comandos disponíveis. diff --git a/src/apps/chatwoot/i18n/locales/ru-RU.yaml b/src/apps/chatwoot/i18n/locales/ru-RU.yaml index cb6c956d..9e6f078b 100644 --- a/src/apps/chatwoot/i18n/locales/ru-RU.yaml +++ b/src/apps/chatwoot/i18n/locales/ru-RU.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ Настройки для сессии WhatsApp **{{{name}}}** были обновлены. +cli.usage.arguments: |- + [действие] + cli.usage.command: |- [команда] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Сервер**: +cli.cmd.root.sub.sync: |- + 🔄️ **Синхронизация**: + cli.cmd.session.status.description: |- Получите текущий статус сессии. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Сделайте снимок экрана сессии (доступно только с движком **WEBJS**). +cli.cmd.contacts.summary: |- + Загрузить контакты WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Выполните `contacts pull`, чтобы запустить синхронизацию, или `contacts status`, чтобы проверить прогресс. + +cli.cmd.contacts.action.description: |- + Используйте `start`, чтобы начать загрузку контактов, или `status`, чтобы проверить активное задание. + +cli.cmd.contacts.pull.description: |- + Начните импорт контактов WhatsApp в ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Количество контактов в каждой партии. + +cli.cmd.contacts.pull.option.delay-contact: |- + Задержка между обработкой контактов, чтобы избежать ограничений при загрузке аватаров. + +cli.cmd.contacts.pull.option.delay-batch: |- + Задержка между партиями контактов. + +cli.cmd.contacts.pull.option.avatar: |- + Выберите, как загружать аватары контактов: `skip`, `if-missing` или `update`. + +cli.cmd.contacts.pull.option.groups: |- + Включить контакты групп WhatsApp в синхронизацию. + +cli.cmd.contacts.pull.option.no-lids: |- + Пропускать контакты LID WhatsApp (JID привязанного устройства) при синхронизации. + +cli.cmd.contacts.pull.option.no-attributes: |- + Не загружать атрибуты контакта, если он уже существует (ускоряет процесс). + +cli.cmd.contacts.pull.already-running: |- + ⏳ Задание **Pull Contacts** уже выполняется. Подождите его завершения или остановите перед запуском нового. + + Отправьте `contacts status` для подробностей. + +cli.cmd.contacts.pull.queued: |- + 👥 Задание **Pull Contacts** поставлено в очередь. + + Мы будем отправлять обновления прогресса каждые **{{batch}}** контактов. + Загрузка контактов может занять время — проверяйте актуальный статус в любое время командой `contacts status`. + +cli.cmd.options.job.attempts: |- + Число повторных попыток до пометки задания как неуспешного. + +cli.cmd.options.job.timeout: |- + Максимальное время выполнения перед повторной попыткой (поддерживает значения `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Показать статус текущего задания загрузки. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Активное задание **Pull Contacts** не найдено. Запустите его командой `contacts pull`. + cli.cmd.server.description: |- Управляйте сервером. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Глобальные опции:** +task.contacts.status: |- + 👤 Загрузка контактов: **{{state}}** + - ✅ Успешно: {{{progress.ok}}} контактов + - ⚪ Пропущено: {{{progress.skipped}}} контактов + - ⚠️ Ошибки: {{{progress.errors}}} контактов + + {{#job}} + ⌛ Хронология: + - В очереди: {{{timestamp}}} + {{#processedOn}} + - Начато: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Завершено: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Подробности: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Идёт загрузка контактов... + +task.contacts.progress: |- + Загрузка контактов: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Отправьте **help**, чтобы получить список доступных команд. diff --git a/src/apps/chatwoot/i18n/locales/tr-TR.yaml b/src/apps/chatwoot/i18n/locales/tr-TR.yaml index f3406d1e..83e92496 100644 --- a/src/apps/chatwoot/i18n/locales/tr-TR.yaml +++ b/src/apps/chatwoot/i18n/locales/tr-TR.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ WhatsApp oturumu **{{{name}}}** için ayarlar güncellendi. +cli.usage.arguments: |- + [eylem] + cli.usage.command: |- [komut] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Sunucu**: +cli.cmd.root.sub.sync: |- + 🔄️ **Eşitleme**: + cli.cmd.session.status.description: |- Geçerli oturum durumunu alın. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Oturumun ekran görüntüsünü alın (yalnızca **WEBJS** motoru ile kullanılabilir). +cli.cmd.contacts.summary: |- + WhatsApp kişilerini içe aktarın. + +cli.cmd.contacts.description: |- + 👤 `contacts pull` komutunu çalıştırarak bir eşitleme başlatın veya `contacts status` ile ilerlemeyi kontrol edin. + +cli.cmd.contacts.action.description: |- + `start` ile kişi çekmeyi başlatın veya `status` ile etkin görevi kontrol edin. + +cli.cmd.contacts.pull.description: |- + WhatsApp kişilerini ChatWoot'a çekmeye başlayın. + +cli.cmd.contacts.pull.option.batch: |- + Her partide çekilecek kişi sayısı. + +cli.cmd.contacts.pull.option.delay-contact: |- + Avatarları çekerken limitlere takılmamak için her kişi arasında bekleme süresi. + +cli.cmd.contacts.pull.option.delay-batch: |- + Kişi partileri arasındaki bekleme süresi. + +cli.cmd.contacts.pull.option.avatar: |- + Kişi avatarlarının nasıl çekileceğini seçin: `skip`, `if-missing` veya `update`. + +cli.cmd.contacts.pull.option.groups: |- + WhatsApp grup kişilerini senkronizasyona dahil et. + +cli.cmd.contacts.pull.option.no-lids: |- + WhatsApp LID kişilerini (bağlı cihaz JID'leri) senkronizasyonda atla. + +cli.cmd.contacts.pull.option.no-attributes: |- + Kişi zaten varsa özelliklerini çekmeyin (süreci hızlandırır). + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** görevi zaten çalışıyor. Bitmesini bekleyin veya yenisini başlatmadan önce durdurun. + + Ayrıntılar için `contacts status` gönderin. + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** görevi sıraya alındı. + + Her **{{batch}}** kişide bir ilerleme güncellemesi paylaşacağız. + Kişileri çekmek zaman alabilir — en güncel durumu istediğiniz zaman `contacts status` ile kontrol edin. + +cli.cmd.options.job.attempts: |- + Görevi başarısız saymadan önce yapılacak yeniden deneme sayısı. + +cli.cmd.options.job.timeout: |- + Görev yeniden denenmeden önce çalışmasına izin verilen en uzun süre (`30s`, `5m`, `1h` gibi değerleri destekler). + +cli.cmd.contacts.status.description: |- + Devam eden kişi çekme görevinin durumunu gösterin. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Aktif bir **Pull Contacts** görevi bulunamadı. `contacts pull` ile bir tane başlatın. + cli.cmd.server.description: |- Sunucuyu yönetin. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Genel seçenekler:** +task.contacts.status: |- + 👤 Kişi çekme: **{{state}}** + - ✅ Başarılı: {{{progress.ok}}} kişi + - ⚪ Atlanan: {{{progress.skipped}}} kişi + - ⚠️ Hatalar: {{{progress.errors}}} kişi + + {{#job}} + ⌛ Zaman çizelgesi: + - Sıraya alındı: {{{timestamp}}} + {{#processedOn}} + - Başladı: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Bitti: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Ayrıntılar: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Kişiler çekiliyor... + +task.contacts.progress: |- + Kişi çekme: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Kullanılabilir komutların listesini almak için **help** gönderin. diff --git a/src/apps/chatwoot/i18n/locales/uk-UA.yaml b/src/apps/chatwoot/i18n/locales/uk-UA.yaml index ae213e3c..d767621a 100644 --- a/src/apps/chatwoot/i18n/locales/uk-UA.yaml +++ b/src/apps/chatwoot/i18n/locales/uk-UA.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ Налаштування для сесії WhatsApp **{{{name}}}** були оновлені. +cli.usage.arguments: |- + [дія] + cli.usage.command: |- [команда] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **Сервер**: +cli.cmd.root.sub.sync: |- + 🔄️ **Синхронізація**: + cli.cmd.session.status.description: |- Отримайте поточний статус сесії. @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- Зробіть знімок екрана сесії (доступно лише з рушієм **WEBJS**). +cli.cmd.contacts.summary: |- + Отримати контакти WhatsApp. + +cli.cmd.contacts.description: |- + 👤 Запустіть `contacts pull`, щоб почати синхронізацію, або `contacts status`, щоб відстежити прогрес. + +cli.cmd.contacts.action.description: |- + Використайте `start`, щоб почати отримання контактів, або `status`, щоб перевірити активне завдання. + +cli.cmd.contacts.pull.description: |- + Почніть імпортувати контакти WhatsApp у ChatWoot. + +cli.cmd.contacts.pull.option.batch: |- + Кількість контактів у кожній партії. + +cli.cmd.contacts.pull.option.delay-contact: |- + Затримка між кожним контактом, щоб уникнути обмежень під час завантаження аватарів. + +cli.cmd.contacts.pull.option.delay-batch: |- + Затримка між партіями контактів. + +cli.cmd.contacts.pull.option.avatar: |- + Виберіть, як завантажувати аватари контактів: `skip`, `if-missing` чи `update`. + +cli.cmd.contacts.pull.option.groups: |- + Додати контакти груп WhatsApp до синхронізації. + +cli.cmd.contacts.pull.option.no-lids: |- + Пропускати контакти LID WhatsApp (JID підключеного пристрою) під час синхронізації. + +cli.cmd.contacts.pull.option.no-attributes: |- + Не завантажуйте атрибути контакту, якщо він уже існує (це пришвидшує процес). + +cli.cmd.contacts.pull.already-running: |- + ⏳ Завдання **Pull Contacts** вже виконується. Дочекайтеся завершення або зупиніть його перед запуском нового. + + Надішліть `contacts status`, щоб отримати деталі. + +cli.cmd.contacts.pull.queued: |- + 👥 Завдання **Pull Contacts** поставлено в чергу. + + Ми надішлемо оновлення прогресу після кожних **{{batch}}** контактів. + Отримання контактів може зайняти час — перевіряйте актуальний статус будь-коли за допомогою `contacts status`. + +cli.cmd.options.job.attempts: |- + Кількість повторних спроб перед позначенням завдання як невдалого. + +cli.cmd.options.job.timeout: |- + Максимальний час виконання перед повторною спробою (підтримуються значення `30s`, `5m`, `1h`). + +cli.cmd.contacts.status.description: |- + Показати статус поточного завдання отримання. + +cli.cmd.contacts.status.not-found: |- + ℹ️ Активне завдання **Pull Contacts** не знайдено. Запустіть його командою `contacts pull`. + cli.cmd.server.description: |- Керуйте сервером. @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **Глобальні опції:** +task.contacts.status: |- + 👤 Отримання контактів: **{{state}}** + - ✅ Успішно: {{{progress.ok}}} контактів + - ⚪ Пропущено: {{{progress.skipped}}} контактів + - ⚠️ Помилки: {{{progress.errors}}} контактів + + {{#job}} + ⌛ Хронологія: + - У черзі: {{{timestamp}}} + {{#processedOn}} + - Початок: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - Завершення: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + Деталі: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + Отримання контактів... + +task.contacts.progress: |- + Отримання контактів: ✅ {{progress.ok}}, ⚪ {{progress.skipped}}, ⚠️ {{progress.errors}} + app.help.reminder: |- Надішліть **help**, щоб отримати список доступних команд. diff --git a/src/apps/chatwoot/i18n/locales/ur-PK.yaml b/src/apps/chatwoot/i18n/locales/ur-PK.yaml index 4613ad2e..cfc30898 100644 --- a/src/apps/chatwoot/i18n/locales/ur-PK.yaml +++ b/src/apps/chatwoot/i18n/locales/ur-PK.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ واٹس ایپ سیشن **{{{name}}}** کی ترتیبات اپڈیٹ کر دی گئی ہیں۔ +cli.usage.arguments: |- + [عمل] + cli.usage.command: |- [کمانڈ] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **سرور**: +cli.cmd.root.sub.sync: |- + 🔄️ **ہم وقت سازی**: + cli.cmd.session.status.description: |- موجودہ سیشن کی حالت حاصل کریں۔ @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- سیشن کا اسکرین شاٹ لیں (صرف **WEBJS** انجن کے ساتھ دستیاب)۔ +cli.cmd.contacts.summary: |- + واٹس ایپ روابط نکالیں۔ + +cli.cmd.contacts.description: |- + 👤 ہم وقت سازی شروع کرنے کے لیے `contacts pull` چلائیں یا پیش رفت دیکھنے کے لیے `contacts status` چلائیں۔ + +cli.cmd.contacts.action.description: |- + `start` سے روابط نکالنا شروع کریں یا `status` سے فعال کام چیک کریں۔ + +cli.cmd.contacts.pull.description: |- + واٹس ایپ روابط کو ChatWoot میں لانا شروع کریں۔ + +cli.cmd.contacts.pull.option.batch: |- + ہر بیچ میں نکالے جانے والے رابطوں کی تعداد۔ + +cli.cmd.contacts.pull.option.delay-contact: |- + ہر رابطے کے درمیان وقفہ تاکہ اوتار نکالتے وقت ریٹ لمٹس سے بچا جا سکے۔ + +cli.cmd.contacts.pull.option.delay-batch: |- + رابطوں کے بیچوں کے درمیان وقفہ۔ + +cli.cmd.contacts.pull.option.avatar: |- + یہ منتخب کریں کہ رابطوں کے اوتار کس طرح نکالے جائیں: `skip`, `if-missing`, یا `update`. + +cli.cmd.contacts.pull.option.groups: |- + واٹس ایپ گروپ روابط کو ہم وقت سازی میں شامل کریں۔ + +cli.cmd.contacts.pull.option.no-lids: |- + واٹس ایپ LID روابط (لنک شدہ ڈیوائس JID) کو ہم وقت سازی کے دوران چھوڑ دیں۔ + +cli.cmd.contacts.pull.option.no-attributes: |- + اگر رابطہ پہلے سے موجود ہو تو اس کے خواص نہ نکالیں (اس سے عمل تیز ہوتا ہے)۔ + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** کام پہلے ہی چل رہا ہے۔ اسے مکمل ہونے دیں یا نیا شروع کرنے سے پہلے روک دیں۔ + + تفصیلات کے لیے `contacts status` بھیجیں۔ + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** کام قطار میں لگا دیا گیا ہے۔ + + ہم ہر **{{batch}}** رابطوں کے بعد پیش رفت کی تازہ کاری شیئر کریں گے۔ + روابط نکالنے میں وقت لگ سکتا ہے — کسی بھی وقت `contacts status` کے ذریعے تازہ ترین حالت دیکھیں۔ + +cli.cmd.options.job.attempts: |- + کام کو ناکام قرار دینے سے پہلے دوبارہ کوششوں کی تعداد۔ + +cli.cmd.options.job.timeout: |- + کام کو دوبارہ چلانے سے پہلے زیادہ سے زیادہ مدت (جیسے `30s`, `5m`, `1h`)۔ + +cli.cmd.contacts.status.description: |- + موجودہ نکاسی کے کام کی حالت دکھائیں۔ + +cli.cmd.contacts.status.not-found: |- + ℹ️ کوئی فعال **Pull Contacts** کام نہیں ملا۔ `contacts pull` سے ایک شروع کریں۔ + cli.cmd.server.description: |- سرور کا انتظام کریں۔ @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **عالمی اختیارات:** +task.contacts.status: |- + 👤 رابطے نکالنا: **{{state}}** + - ✅ کامیابی: {{{progress.ok}}} روابط + - ⚪ چھوڑے گئے: {{{progress.skipped}}} روابط + - ⚠️ غلطیاں: {{{progress.errors}}} روابط + + {{#job}} + ⌛ ٹائم لائن: + - قطار میں: {{{timestamp}}} + {{#processedOn}} + - آغاز: {{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - اختتام: {{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + تفصیلات: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + روابط نکالے جا رہے ہیں... + +task.contacts.progress: |- + رابطے نکالنا: ✅ {{progress.ok}}، ⚪ {{progress.skipped}}، ⚠️ {{progress.errors}} + app.help.reminder: |- دستیاب کمانڈز کی فہرست حاصل کرنے کے لیے **help** بھیجیں۔ diff --git a/src/apps/chatwoot/i18n/locales/zh-CN.yaml b/src/apps/chatwoot/i18n/locales/zh-CN.yaml index 7e33ce15..02948d25 100644 --- a/src/apps/chatwoot/i18n/locales/zh-CN.yaml +++ b/src/apps/chatwoot/i18n/locales/zh-CN.yaml @@ -117,6 +117,9 @@ app.updated.message: |- ✅ WhatsApp 会话 **{{{name}}}** 的设置已更新。 +cli.usage.arguments: |- + [操作] + cli.usage.command: |- [命令] @@ -138,6 +141,9 @@ cli.cmd.root.sub.session: |- cli.cmd.root.sub.server: |- 🖥️ **服务器**: +cli.cmd.root.sub.sync: |- + 🔄️ **同步**: + cli.cmd.session.status.description: |- 获取当前会话状态。 @@ -159,6 +165,62 @@ cli.cmd.session.qr.description: |- cli.cmd.session.screenshot.description: |- 截取会话截图(仅适用于 **WEBJS** 引擎)。 +cli.cmd.contacts.summary: |- + 拉取 WhatsApp 联系人。 + +cli.cmd.contacts.description: |- + 👤 运行 `contacts pull` 启动同步,或运行 `contacts status` 查看进度。 + +cli.cmd.contacts.action.description: |- + 使用 `start` 开始拉取联系人,或使用 `status` 查看进行中的任务。 + +cli.cmd.contacts.pull.description: |- + 开始将 WhatsApp 联系人拉取到 ChatWoot。 + +cli.cmd.contacts.pull.option.batch: |- + 每批次拉取的联系人数量。 + +cli.cmd.contacts.pull.option.delay-contact: |- + 在每个联系人之间延迟,以避免拉取头像时触发频率限制。 + +cli.cmd.contacts.pull.option.delay-batch: |- + 在联系人批次之间的延迟。 + +cli.cmd.contacts.pull.option.avatar: |- + 选择如何拉取联系人头像:`skip`、`if-missing` 或 `update`。 + +cli.cmd.contacts.pull.option.groups: |- + 在同步中包含 WhatsApp 群组联系人。 + +cli.cmd.contacts.pull.option.no-lids: |- + 在同步中跳过 WhatsApp LID 联系人(关联设备 JID)。 + +cli.cmd.contacts.pull.option.no-attributes: |- + 如果联系人已存在,则不要拉取其属性(可加快流程)。 + +cli.cmd.contacts.pull.already-running: |- + ⏳ **Pull Contacts** 任务已在运行。请等待完成或先停止再启动新的。 + + 发送 `contacts status` 获取详情。 + +cli.cmd.contacts.pull.queued: |- + 👥 **Pull Contacts** 任务已排队。 + + 我们会每 **{{batch}}** 个联系人更新一次进度。 + 拉取联系人可能需要一些时间——随时使用 `contacts status` 查看最新状态。 + +cli.cmd.options.job.attempts: |- + 任务标记为失败前的最大重试次数。 + +cli.cmd.options.job.timeout: |- + 任务重新尝试前允许运行的最长时间(支持 `30s`、`5m`、`1h` 等值)。 + +cli.cmd.contacts.status.description: |- + 显示当前拉取任务的状态。 + +cli.cmd.contacts.status.not-found: |- + ℹ️ 未找到正在运行的 **Pull Contacts** 任务。使用 `contacts pull` 启动一个。 + cli.cmd.server.description: |- 管理服务器。 @@ -186,6 +248,34 @@ cli.help.options.defaultGroup: |- cli.help.globalOptions.title: |- **全局选项:** +task.contacts.status: |- + 👤 联系人拉取:**{{state}}** + - ✅ 成功:{{{progress.ok}}} 位联系人 + - ⚪ 跳过:{{{progress.skipped}}} 位联系人 + - ⚠️ 错误:{{{progress.errors}}} 位联系人 + + {{#job}} + ⌛ 时间线: + - 排队:{{{timestamp}}} + {{#processedOn}} + - 开始:{{{processedOn}}} + {{/processedOn}} + {{#finishedOn}} + - 完成:{{{finishedOn}}} + {{/finishedOn}} + {{/job}} + + {{#error}} + 详情: + [{{{details.text}}}]({{{details.url}}}) + {{/error}} + +task.contacts.started: |- + 正在拉取联系人... + +task.contacts.progress: |- + 联系人拉取:✅ {{progress.ok}},⚪ {{progress.skipped}},⚠️ {{progress.errors}} + app.help.reminder: |- 发送 **help** 获取可用命令列表。 diff --git a/src/apps/chatwoot/services/ChatWootScheduleService.ts b/src/apps/chatwoot/services/ChatWootScheduleService.ts index 48cd1491..525a7b91 100644 --- a/src/apps/chatwoot/services/ChatWootScheduleService.ts +++ b/src/apps/chatwoot/services/ChatWootScheduleService.ts @@ -1,8 +1,10 @@ import { InjectQueue } from '@nestjs/bullmq'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Queue } from 'bullmq'; import { QueueName } from '../consumers/QueueName'; +import { InjectPinoLogger, PinoLogger } from 'nestjs-pino'; +import { ContactsPullRemove } from '@waha/apps/chatwoot/cli/cmd.contacts'; /** * Service for scheduling ChatWoot tasks @@ -11,10 +13,14 @@ import { QueueName } from '../consumers/QueueName'; @Injectable() export class ChatWootScheduleService { constructor( + @InjectPinoLogger('DashboardConfigService') + protected logger: PinoLogger, @InjectQueue(QueueName.SCHEDULED_MESSAGE_CLEANUP) private readonly messageCleanupQueue: Queue, @InjectQueue(QueueName.SCHEDULED_CHECK_VERSION) private readonly checkVersionQueue: Queue, + @InjectQueue(QueueName.TASK_CONTACTS_PULL) + private readonly importContactsQueue: Queue, ) {} /** @@ -58,9 +64,27 @@ export class ChatWootScheduleService { await this.checkVersionQueue.removeJobScheduler( this.JobId(QueueName.SCHEDULED_CHECK_VERSION, appId), ); + + // contacts + ContactsPullRemove(this.importContactsQueue, appId, this.logger).catch( + (reason) => { + // Ignore errors + this.logger.warn( + `Failed to remove "contacts" job for app ${appId}, session ${sessionName}: ${reason}`, + ); + }, + ); } + /** + * DEPRECATED - for backward compatability + * Use SingleJobId if you want to have single job in the queue by app + */ private JobId(queue: QueueName, appId: string) { return `${queue} | ${appId}`; } + + static SingleJobId(appId: string) { + return `${appId}`; + } } diff --git a/src/apps/chatwoot/session/WAHASelf.ts b/src/apps/chatwoot/session/WAHASelf.ts index 5233d810..d79f5b7f 100644 --- a/src/apps/chatwoot/session/WAHASelf.ts +++ b/src/apps/chatwoot/session/WAHASelf.ts @@ -12,6 +12,12 @@ import { import { SessionInfo } from '@waha/structures/sessions.dto'; import axios, { AxiosInstance } from 'axios'; import { Auth } from '@waha/core/auth/config'; +import { ContactSortField } from '@waha/structures/contacts.dto'; +import { PaginationParams } from '@waha/structures/pagination.dto'; + +export interface RequestOptions { + signal?: AbortSignal; +} export class WAHASelf { public client: AxiosInstance; @@ -33,57 +39,78 @@ export class WAHASelf { }); } - async fetch(url: string): Promise { + async fetch(url: string, opts?: RequestOptions): Promise { const response = await this.client.get(url, { responseType: 'arraybuffer', + signal: opts?.signal, }); return Buffer.from(response.data); } - async qr(session: string): Promise { + async qr(session: string, opts?: RequestOptions): Promise { const url = `/api/${session}/auth/qr`; - return await this.fetch(url); + return await this.fetch(url, opts); } - async screenshot(session: string): Promise { + async screenshot(session: string, opts?: RequestOptions): Promise { const url = `/api/screenshot?session=${session}`; - return await this.fetch(url); + return await this.fetch(url, opts); } - async restart(session: string): Promise { + async restart(session: string, opts?: RequestOptions): Promise { const url = `/api/sessions/${session}/restart`; - return await this.client.post(url); + return await this.client.post(url, undefined, { signal: opts?.signal }); } - async logout(session: string): Promise { + async logout(session: string, opts?: RequestOptions): Promise { const url = `/api/sessions/${session}/logout`; - return await this.client.post(url); + return await this.client.post(url, undefined, { signal: opts?.signal }); } - async stop(session: string): Promise { + async stop(session: string, opts?: RequestOptions): Promise { const url = `/api/sessions/${session}/stop`; - return await this.client.post(url); + return await this.client.post(url, undefined, { signal: opts?.signal }); } - async get(session: string): Promise { + async get(session: string, opts?: RequestOptions): Promise { const url = `/api/sessions/${session}/`; - return await this.client.get(url).then((response) => response.data); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data); } - async getContact(session: string, contactId: string) { + async getContacts( + session: string, + page: PaginationParams, + opts?: RequestOptions, + ) { + const url = `/api/contacts/all`; + const params = { + session: session, + ...page, + sortBy: ContactSortField.ID, + sortOrder: ContactSortField.ID, + }; + return await this.client + .get(url, { params: params, signal: opts?.signal }) + .then((response) => response.data); + } + + async getContact(session: string, contactId: string, opts?: RequestOptions) { const url = `/api/contacts`; const params = { session: session, contactId: contactId, }; return await this.client - .get(url, { params: params }) + .get(url, { params: params, signal: opts?.signal }) .then((response) => response.data); } async contactCheckExists( session: string, phone: string, + opts?: RequestOptions, ): Promise { const url = `/api/contacts/check-exists`; const params = { @@ -91,99 +118,162 @@ export class WAHASelf { session: session, }; return await this.client - .get(url, { params: params }) + .get(url, { params: params, signal: opts?.signal }) .then((response) => response.data); } - async getGroup(session: string, groupId: string) { + async getGroup(session: string, groupId: string, opts?: RequestOptions) { const url = `/api/${session}/groups/${groupId}`; - return await this.client.get(url).then((response) => response.data); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data); } - async getChannel(session: string, channelId: string): Promise { + async getChannel( + session: string, + channelId: string, + opts?: RequestOptions, + ): Promise { const url = `/api/${session}/channels/${channelId}`; - return await this.client.get(url).then((response) => response.data); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data); } async getChatPicture( session: string, chatId: string, + opts?: RequestOptions, ): Promise { const url = `/api/${session}/chats/${chatId}/picture`; - return await this.client.get(url).then((response) => response.data?.url); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data?.url); } - async sendText(body: MessageTextRequest): Promise { + async sendText( + body: MessageTextRequest, + opts?: RequestOptions, + ): Promise { const url = `/api/sendText`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async sendImage(body: MessageImageRequest): Promise { + async sendImage( + body: MessageImageRequest, + opts?: RequestOptions, + ): Promise { const url = `/api/sendImage`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async sendVideo(body: MessageVideoRequest): Promise { + async sendVideo( + body: MessageVideoRequest, + opts?: RequestOptions, + ): Promise { const url = `/api/sendVideo`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async sendVoice(body: MessageVoiceRequest): Promise { + async sendVoice( + body: MessageVoiceRequest, + opts?: RequestOptions, + ): Promise { const url = `/api/sendVoice`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async sendFile(body: MessageFileRequest): Promise { + async sendFile( + body: MessageFileRequest, + opts?: RequestOptions, + ): Promise { const url = `/api/sendFile`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async deleteMessage(session: string, chatId: string, messageId: string) { + async deleteMessage( + session: string, + chatId: string, + messageId: string, + opts?: RequestOptions, + ) { const url = `/api/${session}/chats/${chatId}/messages/${messageId}`; - return await this.client.delete(url).then((response) => response.data); + return await this.client + .delete(url, { signal: opts?.signal }) + .then((response) => response.data); } - async startTyping(body: ChatRequest) { + async startTyping(body: ChatRequest, opts?: RequestOptions) { const url = `/api/startTyping`; - return await this.client.post(url, body).then((response) => response.data); + return await this.client + .post(url, body, { signal: opts?.signal }) + .then((response) => response.data); } - async stopTyping(body: ChatRequest) { + async stopTyping(body: ChatRequest, opts?: RequestOptions) { const url = `/api/stopTyping`; - return await this.client.post(url, body); + return await this.client.post(url, body, { signal: opts?.signal }); } - async readMessages(session: string, chatId: string) { + async readMessages(session: string, chatId: string, opts?: RequestOptions) { const url = `/api/${session}/chats/${chatId}/messages/read`; - return await this.client.post(url).then((response) => response.data); + return await this.client + .post(url, undefined, { signal: opts?.signal }) + .then((response) => response.data); } - async findPNByLid(session: string, lid: string): Promise { + async findPNByLid( + session: string, + lid: string, + opts?: RequestOptions, + ): Promise { const url = `/api/${session}/lids/${lid}`; - return await this.client.get(url).then((response) => response.data.pn); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data.pn); } - async findLIDByPN(session: string, pn: string): Promise { + async findLIDByPN( + session: string, + pn: string, + opts?: RequestOptions, + ): Promise { const url = `/api/${session}/lids/pn/${pn}`; - return await this.client.get(url).then((response) => response.data.lid); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data.lid); } /** * Get the server version * @returns Server version information */ - async serverVersion(): Promise { + async serverVersion(opts?: RequestOptions): Promise { const url = `/api/server/version`; - return await this.client.get(url).then((response) => response.data); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data); } /** * Get the server status * @returns Server status information */ - async serverStatus(): Promise { + async serverStatus(opts?: RequestOptions): Promise { const url = `/api/server/status`; - return await this.client.get(url).then((response) => response.data); + return await this.client + .get(url, { signal: opts?.signal }) + .then((response) => response.data); } /** @@ -191,10 +281,13 @@ export class WAHASelf { * @param force Whether to force reboot (true) or gracefully reboot (false) * @returns Server stop response */ - async serverReboot(force: boolean = false): Promise { + async serverReboot( + force: boolean = false, + opts?: RequestOptions, + ): Promise { const url = `/api/server/stop`; return await this.client - .post(url, { force }) + .post(url, { force }, { signal: opts?.signal }) .then((response) => response.data); } } @@ -205,77 +298,87 @@ export class WAHASessionAPI { private api: WAHASelf, ) {} - getContact(contactId: string): Promise { - return this.api.getContact(this.session, contactId); + getContacts(page: PaginationParams, opts?: RequestOptions): Promise { + return this.api.getContacts(this.session, page, opts); } - contactCheckExists(phone: string): Promise { - return this.api.contactCheckExists(this.session, phone); + getContact(contactId: string, opts?: RequestOptions): Promise { + return this.api.getContact(this.session, contactId, opts); } - getGroup(groupId: string): Promise { - return this.api.getGroup(this.session, groupId); + contactCheckExists( + phone: string, + opts?: RequestOptions, + ): Promise { + return this.api.contactCheckExists(this.session, phone, opts); } - getChannel(channelId: string): Promise { - return this.api.getChannel(this.session, channelId); + getGroup(groupId: string, opts?: RequestOptions): Promise { + return this.api.getGroup(this.session, groupId, opts); } - getChatPicture(chatId: string): Promise { - return this.api.getChatPicture(this.session, chatId); + getChannel(channelId: string, opts?: RequestOptions): Promise { + return this.api.getChannel(this.session, channelId, opts); } - sendText(body: MessageTextRequest): Promise { + getChatPicture( + chatId: string, + opts?: RequestOptions, + ): Promise { + return this.api.getChatPicture(this.session, chatId, opts); + } + + sendText(body: MessageTextRequest, opts?: RequestOptions): Promise { body.session = this.session; - return this.api.sendText(body); + return this.api.sendText(body, opts); } - sendImage(body: MessageImageRequest): Promise { + sendImage(body: MessageImageRequest, opts?: RequestOptions): Promise { body.session = this.session; - return this.api.sendImage(body); + return this.api.sendImage(body, opts); } - sendVideo(body: MessageVideoRequest): Promise { + sendVideo(body: MessageVideoRequest, opts?: RequestOptions): Promise { body.session = this.session; - return this.api.sendVideo(body); + return this.api.sendVideo(body, opts); } - sendVoice(body: MessageVoiceRequest): Promise { + sendVoice(body: MessageVoiceRequest, opts?: RequestOptions): Promise { body.session = this.session; - return this.api.sendVoice(body); + return this.api.sendVoice(body, opts); } - sendFile(body: MessageFileRequest): Promise { + sendFile(body: MessageFileRequest, opts?: RequestOptions): Promise { body.session = this.session; - return this.api.sendFile(body); + return this.api.sendFile(body, opts); } - deleteMessage(chatId: string, messageId: string) { - return this.api.deleteMessage(this.session, chatId, messageId); + deleteMessage(chatId: string, messageId: string, opts?: RequestOptions) { + return this.api.deleteMessage(this.session, chatId, messageId, opts); } - startTyping(body: ChatRequest) { + startTyping(body: ChatRequest, opts?: RequestOptions) { body.session = this.session; - return this.api.startTyping(body); + return this.api.startTyping(body, opts); } - stopTyping(body: ChatRequest) { + stopTyping(body: ChatRequest, opts?: RequestOptions) { body.session = this.session; - return this.api.stopTyping(body); + return this.api.stopTyping(body, opts); } - readMessages(chatId: string) { - return this.api.readMessages(this.session, chatId); + readMessages(chatId: string, opts?: RequestOptions) { + return this.api.readMessages(this.session, chatId, opts); } // // Lids // - findPNByLid(lid: string) { - return this.api.findPNByLid(this.session, lid); + findPNByLid(lid: string, opts?: RequestOptions) { + return this.api.findPNByLid(this.session, lid, opts); } - findLIDByPN(pn: string) { - return this.api.findLIDByPN(this.session, pn); + findLIDByPN(pn: string, opts?: RequestOptions) { + return this.api.findLIDByPN(this.session, pn, opts); } } diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts index f1cb9a58..420b5ef0 100644 --- a/src/core/abc/session.abc.ts +++ b/src/core/abc/session.abc.ts @@ -7,6 +7,7 @@ import { IgnoreJidConfig, isJidBroadcast, isJidNewsletter, + isNullJid, JidFilter, } from '@waha/core/utils/jids'; import { @@ -748,7 +749,7 @@ export abstract class WhatsappSession { protected async refreshProfilePicture(id: string) { this.logger.debug(`Refreshing profile picture for id "${id}"...`); // Have no pictures - if (id === '0@c.us' || id === '0@s.whatsapp.net') { + if (isNullJid(id)) { return null; } else if (isJidBroadcast(id)) { return null; diff --git a/src/core/utils/jids.ts b/src/core/utils/jids.ts index 24975512..75cfc31c 100644 --- a/src/core/utils/jids.ts +++ b/src/core/utils/jids.ts @@ -29,11 +29,24 @@ export function isLidUser(jid: string) { return typeof jid === 'string' && jid.endsWith('@lid'); } +export function isNullJid(jid: string) { + if (!jid) { + return false; + } + return jid === '0@c.us' || jid === '0@s.whatsapp.net'; +} + export function isPnUser(jid: string) { - return ( - typeof jid === 'string' && - (jid.endsWith('@s.whatsapp.net') || jid.endsWith('@c.us')) - ); + if (typeof jid !== 'string') { + return false; + } + if (!jid.endsWith('@s.whatsapp.net') && !jid.endsWith('@c.us')) { + return false; + } + if (isNullJid(jid)) { + return false; + } + return true; } export function normalizeJid(jid: string): string { diff --git a/src/structures/contacts.dto.ts b/src/structures/contacts.dto.ts index da00376e..6bd432a3 100644 --- a/src/structures/contacts.dto.ts +++ b/src/structures/contacts.dto.ts @@ -30,7 +30,7 @@ export class ContactProfilePictureQuery extends ContactQuery { refresh?: boolean = false; } -enum ContactSortField { +export enum ContactSortField { ID = 'id', NAME = 'name', } diff --git a/src/utils/abortable.ts b/src/utils/abortable.ts new file mode 100644 index 00000000..681606b9 --- /dev/null +++ b/src/utils/abortable.ts @@ -0,0 +1,44 @@ +/** + * Convert an AbortSignal into a Promise that rejects when aborted. + * + * Example: + * const signal = AbortSignal.timeout(5000); + * await Promise.race([ + * slowOperation(signal), + * SignalToPromise(signal), + * ]); + */ +export function SignalToPromise(signal: AbortSignal): Promise { + signal.throwIfAborted(); + return new Promise((_, reject) => { + const onAbort = () => { + signal.removeEventListener('abort', onAbort); + reject( + signal.reason instanceof DOMException + ? signal.reason + : new DOMException('The operation was aborted', 'AbortError'), + ); + }; + signal.addEventListener('abort', onAbort, { once: true }); + }); +} + +/** + * Race an already-started promise against an AbortSignal. + * The promise settles normally unless the signal aborts first. + * + * Example: + * const signal = AbortSignal.any([ + * AbortSignal.timeout(10_000), + * controller.signal, + * ]); + * + * await SignalRace(doSomethingAsync(args, signal), signal); + */ +export function SignalRace( + promise: Promise, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + return Promise.race([promise, SignalToPromise(signal)]); +} diff --git a/yarn.lock b/yarn.lock index 54699fa5..99fb8600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12596,6 +12596,7 @@ __metadata: long: ^5.3.2 mime-types: ^2.1.27 mongodb: 6.9.0 + ms: ^2.1.3 mustache: ^4.2.0 nestjs-pino: ^4.1.0 node-cache: 5.1.2