{"version":3,"file":"client.mjs","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import type { ClientConfiguration } from \"~/config/config\";\nimport type { IRCMessage } from \"~/message/irc/irc-message\";\nimport type { ClientMixin, ConnectionMixin } from \"~/mixins/base-mixin\";\nimport type { ConnectionPool } from \"~/mixins/connection-pool\";\nimport { BaseClient } from \"./base-client\";\nimport { SingleConnection } from \"./connection\";\nimport { ClientError } from \"./errors\";\nimport { PrivmsgMessage } from \"~/message\";\nimport { ConnectionRateLimiter } from \"~/mixins/ratelimiters/connection\";\nimport { JoinRateLimiter } from \"~/mixins/ratelimiters/join\";\nimport { PrivmsgMessageRateLimiter } from \"~/mixins/ratelimiters/privmsg\";\nimport { RoomStateTracker } from \"~/mixins/roomstate-tracker\";\nimport { UserStateTracker } from \"~/mixins/userstate-tracker\";\nimport { joinChannel, joinNothingToDo } from \"~/operations/join\";\nimport { joinAll } from \"~/operations/join-all\";\nimport { partChannel, partNothingToDo } from \"~/operations/part\";\nimport { sendPing } from \"~/operations/ping\";\nimport { sendPrivmsg } from \"~/operations/privmsg\";\nimport { me, reply, say } from \"~/operations/say\";\nimport { anyCauseInstanceof } from \"~/utils/any-cause-instanceof\";\nimport { debugLogger } from \"~/utils/debug-logger\";\nimport { findAndPushToEnd } from \"~/utils/find-and-push-to-end\";\nimport { removeInPlace } from \"~/utils/remove-in-place\";\nimport { toChunked } from \"~/utils/to-chunked\";\nimport { unionSets } from \"~/utils/union-sets\";\nimport { correctChannelName, validateChannelName } from \"~/validation/channel\";\nimport { validateMessageId } from \"~/validation/reply\";\n\nconst log = debugLogger(\"dank-twitch-irc:client\");\n\nexport type ConnectionPredicate = (conn: SingleConnection) => boolean;\n\nexport class ChatClient extends BaseClient {\n  public get wantedChannels(): Set<string> {\n    return unionSets(this.connections.map((c) => c.wantedChannels));\n  }\n\n  public get joinedChannels(): Set<string> {\n    return unionSets(this.connections.map((c) => c.joinedChannels));\n  }\n\n  public readonly userStateTracker: UserStateTracker;\n  public readonly roomStateTracker: RoomStateTracker;\n  public connectionPool?: ConnectionPool;\n  public readonly connectionMixins: ConnectionMixin[] = [];\n\n  public readonly connections: SingleConnection[] = [];\n  private activeWhisperConn: SingleConnection | undefined;\n\n  public constructor(configuration?: ClientConfiguration) {\n    super(configuration);\n\n    this.userStateTracker = new UserStateTracker(this);\n    this.use(this.userStateTracker);\n    this.roomStateTracker = new RoomStateTracker();\n    this.use(this.roomStateTracker);\n\n    if (this.configuration.installDefaultMixins) {\n      this.use(new ConnectionRateLimiter(this));\n      this.use(new PrivmsgMessageRateLimiter(this));\n      this.use(new JoinRateLimiter(this));\n    }\n\n    this.on(\"error\", (error) => {\n      if (anyCauseInstanceof(error, ClientError)) {\n        process.nextTick(() => {\n          this.emitClosed(error);\n          for (const conn of this.connections) conn.destroy(error);\n        });\n      }\n    });\n\n    this.on(\"close\", () => {\n      for (const conn of this.connections) conn.close();\n    });\n  }\n\n  public async connect(): Promise<void> {\n    this.requireConnection();\n    if (!this.ready) {\n      await new Promise<void>((resolve) => this.once(\"ready\", () => resolve()));\n    }\n  }\n\n  public close(): void {\n    // -> connections are close()d via \"close\" event listener\n    this.emitClosed();\n  }\n\n  public destroy(error?: Error): void {\n    // we emit onError before onClose just like the standard node.js core modules do\n    if (error == null) {\n      this.emitClosed();\n    } else {\n      this.emitError(error);\n      this.emitClosed(error);\n    }\n  }\n\n  /**\n   * Sends a raw IRC command to the server, e.g. <code>JOIN #forsen</code>.\n   *\n   * Throws an exception if the passed command contains one or more newline characters.\n   *\n   * @param command Raw IRC command.\n   */\n  public sendRaw(command: string): void {\n    this.requireConnection().sendRaw(command);\n  }\n\n  public async join(channelName: string): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n\n    if (this.connections.some((c) => joinNothingToDo(c, channelName))) {\n      // are we joined already?\n      return;\n    }\n\n    // check if any existing conn wants this channel (and is not joined), if not find any conn that has space\n    // if none is found, create a new connection\n    const conn =\n      findAndPushToEnd(this.connections, (conn) =>\n        conn.wantedChannels.has(channelName),\n      ) ??\n      this.requireConnection(\n        maxJoinedChannels(this.configuration.maxChannelCountPerConnection),\n      );\n\n    await joinChannel(conn, channelName);\n  }\n\n  public async part(channelName: string): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n\n    if (this.connections.every((c) => partNothingToDo(c, channelName))) {\n      // are we parted already?\n      return;\n    }\n\n    const conn = this.requireConnection(\n      (c) => !partNothingToDo(c, channelName),\n    );\n    await partChannel(conn, channelName);\n  }\n\n  public async joinAll(\n    channelNames: string[],\n  ): Promise<Record<string, Error | undefined>> {\n    channelNames = channelNames.map((v) => {\n      v = correctChannelName(v);\n      validateChannelName(v);\n      return v;\n    });\n\n    const needToJoin = new Set(\n      channelNames.filter(\n        (channelName) =>\n          !this.connections.some((c) => joinNothingToDo(c, channelName)),\n      ),\n    );\n\n    const chunks: [SingleConnection, string[]][] = this.connections.map(\n      (conn) => [conn, []],\n    );\n\n    // add channels to the connections that wants them\n    for (const [conn, list] of chunks) {\n      // list of channels this conn wants but is not joined to\n      const channels = [...conn.wantedChannels].filter((channelName) =>\n        needToJoin.has(channelName),\n      );\n\n      list.push(...channels);\n      for (const channel of channels) {\n        needToJoin.delete(channel);\n      }\n    }\n\n    // now we can add channels to the connections that have space\n    for (const [conn, list] of chunks) {\n      // number of channels this conn can still join\n      const count =\n        this.configuration.maxChannelCountPerConnection -\n        conn.wantedChannels.size;\n      const channels = [...needToJoin].slice(0, count);\n\n      list.push(...channels);\n      for (const channel of channels) {\n        needToJoin.delete(channel);\n      }\n    }\n\n    // finally, create new connections for the remaining channels\n    for (const chunk of toChunked(\n      [...needToJoin],\n      this.configuration.maxChannelCountPerConnection,\n    )) {\n      chunks.push([this.newConnection(), chunk]);\n    }\n\n    const promises: Promise<Record<string, Error | undefined>>[] = chunks.map(\n      async (args) => joinAll(...args),\n    );\n\n    const errorChunks = await Promise.all(promises);\n    return errorChunks.reduce(\n      (accumulator, chunk) => Object.assign(accumulator, chunk),\n      {},\n    );\n  }\n\n  public async privmsg(channelName: string, message: string): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n    return sendPrivmsg(this.requireConnection(), channelName, message);\n  }\n\n  public async say(channelName: string, message: string): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n    await say(\n      this.requireConnection(mustNotBeJoined(channelName)),\n      channelName,\n      message,\n    );\n  }\n\n  public async me(channelName: string, message: string): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n    await me(\n      this.requireConnection(mustNotBeJoined(channelName)),\n      channelName,\n      message,\n    );\n  }\n\n  /**\n   * @param channelName The channel name you want to reply in.\n   * @param messageId The message ID you want to reply to.\n   * @param message The message you want to send.\n   */\n  public async reply(\n    channelName: string,\n    messageId: string,\n    message: string,\n  ): Promise<void> {\n    channelName = correctChannelName(channelName);\n    validateChannelName(channelName);\n    validateMessageId(messageId);\n    await reply(\n      this.requireConnection(mustNotBeJoined(channelName)),\n      channelName,\n      messageId,\n      message,\n    );\n  }\n\n  public async ping(): Promise<void> {\n    await sendPing(this.requireConnection());\n  }\n\n  public newConnection(): SingleConnection {\n    const conn = new SingleConnection(this.configuration);\n\n    log.debug(`Creating new connection (ID ${conn.connectionId})`);\n\n    for (const mixin of this.connectionMixins) {\n      conn.use(mixin);\n    }\n\n    conn.on(\"connecting\", () => this.emitConnecting());\n    conn.on(\"connect\", () => this.emitConnected());\n    conn.on(\"ready\", () => this.emitReady());\n    conn.on(\"error\", (error) => this.emitError(error));\n    conn.on(\"close\", (hadError) => {\n      if (hadError) {\n        log.warn(`Connection ${conn.connectionId} was closed due to error`);\n      } else {\n        log.debug(`Connection ${conn.connectionId} closed normally`);\n      }\n\n      removeInPlace(this.connections, conn);\n\n      if (this.activeWhisperConn === conn) {\n        this.activeWhisperConn = undefined;\n      }\n\n      if (!this.closed) {\n        this.reconnectFailedConnection(conn);\n      }\n    });\n\n    // forward commands issued by this client\n    conn.on(\"rawCommmand\", (cmd) => this.emit(\"rawCommmand\", cmd));\n\n    // forward events to this client\n    conn.on(\"message\", (message) => {\n      // only forward whispers from the currently active whisper connection\n      if (message.ircCommand === \"WHISPER\") {\n        this.activeWhisperConn ??= conn;\n\n        if (this.activeWhisperConn !== conn) {\n          // message is ignored.\n          return;\n        }\n      }\n\n      this.emitMessage(message);\n    });\n\n    conn.connect();\n\n    this.connections.push(conn);\n    return conn;\n  }\n\n  public use(mixin: ClientMixin): void {\n    mixin.applyToClient(this);\n  }\n\n  private reconnectFailedConnection(conn: SingleConnection): void {\n    // rejoin channels, creates connections on demand\n    const channels = [...conn.wantedChannels];\n\n    if (channels.length > 0) {\n      void this.joinAll(channels);\n    } else if (this.connections.length <= 0) {\n      // this ensures that clients with zero joined channels stay connected (so they can receive whispers)\n      this.requireConnection();\n    }\n\n    this.emit(\"reconnect\", conn);\n  }\n\n  /**\n   * @param options the options object\n   * @param options.filter only messages for which this function returns true will be yielded. This can be used with type guards to create typed async generators, e.g. for only chat messages (PrivmsgMessage).\n   * @param options.limit the generator will end after yielding this many messages\n   * @param options.timeout the generator will end after this many milliseconds\n   * @returns An async generator yielding matching messages\n   */\n  public async *collectMessages<\n    TMessage extends IRCMessage = IRCMessage,\n  >(options?: {\n    filter?: (message: IRCMessage) => message is TMessage;\n    limit?: number;\n    timeout?: number;\n  }): AsyncGenerator<TMessage, void, void> {\n    const { filter, limit, timeout } = options ?? {};\n    const queue: IRCMessage[] = [];\n    let resolveNext: ((value: IRCMessage | null) => void) | null = null;\n    let closed = this.closed;\n    let yielded = 0;\n\n    const onMessage = (message: IRCMessage): void => {\n      if (closed) return;\n      if (filter && !filter(message)) return;\n      if (resolveNext) {\n        resolveNext(message);\n        resolveNext = null;\n      } else {\n        queue.push(message);\n      }\n    };\n\n    const onClose = (): void => {\n      closed = true;\n      if (resolveNext) {\n        resolveNext(null);\n        resolveNext = null;\n      }\n    };\n\n    this.on(\"message\", onMessage);\n    this.on(\"close\", onClose);\n\n    const timer = timeout == null ? null : setTimeout(() => onClose(), timeout);\n\n    try {\n      // eslint-disable-next-line no-unmodified-loop-condition -- it is modified in the onClose callback\n      while (!closed) {\n        if (queue.length > 0) {\n          yield queue.shift() as TMessage;\n          yielded++;\n          if (limit != null && yielded >= limit) return;\n        } else {\n          const message = await new Promise<IRCMessage | null>((resolve) => {\n            resolveNext = resolve;\n          });\n          if (message === null) {\n            return;\n          }\n          yield message as TMessage;\n          yielded++;\n          if (limit != null && yielded >= limit) return;\n        }\n      }\n    } finally {\n      if (timer != null) clearTimeout(timer);\n      this.off(\"message\", onMessage);\n      this.off(\"close\", onClose);\n    }\n  }\n\n  /**\n   * @param options the options object\n   * @param options.filter only chat messages for which this function returns true will be yielded\n   * @param options.limit the generator will end after yielding this many messages\n   * @param options.timeout the generator will end after this many milliseconds\n   * @returns An async generator yielding matching messages\n   */\n  public collectChatMessages(options?: {\n    filter?: (message: PrivmsgMessage) => boolean;\n    limit?: number;\n    timeout?: number;\n  }): AsyncGenerator<PrivmsgMessage, void, void> {\n    return this.collectMessages({\n      ...options,\n      filter: (message): message is PrivmsgMessage =>\n        message instanceof PrivmsgMessage &&\n        (options?.filter?.(message) ?? true),\n    });\n  }\n\n  /**\n   * Finds a connection from the list of connections that satisfies the given predicate,\n   * or if none was found, returns makes a new connection. This means that the given predicate must be specified\n   * in a way that a new connection always satisfies it.\n   *\n   * @param predicate The predicate the connection must fulfill.\n   */\n  public requireConnection(\n    predicate: ConnectionPredicate = () => true,\n  ): SingleConnection {\n    return (\n      findAndPushToEnd(this.connections, predicate) ?? this.newConnection()\n    );\n  }\n}\n\nfunction maxJoinedChannels(maxChannelCount: number): ConnectionPredicate {\n  return (conn) => conn.wantedChannels.size < maxChannelCount;\n}\n\nfunction mustNotBeJoined(channelName: string): ConnectionPredicate {\n  return (conn) =>\n    !conn.wantedChannels.has(channelName) &&\n    !conn.joinedChannels.has(channelName);\n}\n"],"mappings":"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"}