This is an automated email from the ASF dual-hosted git repository.

meonkeys pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fineract-chat-archive.git


The following commit(s) were added to refs/heads/main by this push:
     new c1eef9d  migrate to new repo
c1eef9d is described below

commit c1eef9d09fd179a5afdc3d67ff7376c251662f18
Author: Adam Monsen <[email protected]>
AuthorDate: Sat Feb 7 12:59:45 2026 -0800

    migrate to new repo
---
 .github/workflows/test.yml                         |  23 -
 .github/workflows/update-archive.yml               |  63 ---
 .github/workflows/verify-commits.yml               |  47 --
 .gitignore                                         |   6 -
 Readme.md                                          |  50 +--
 build.gradle                                       |  35 --
 config/archive.properties                          |  23 -
 gradle/wrapper/gradle-wrapper.jar                  | Bin 43462 -> 0 bytes
 gradle/wrapper/gradle-wrapper.properties           |   7 -
 gradlew                                            | 249 -----------
 gradlew.bat                                        |  92 ----
 scripts/verify-signed-commits.sh                   |  97 ----
 settings.gradle                                    |   1 -
 .../fineract/chat/archive/ArchiveConfig.java       | 123 -----
 .../fineract/chat/archive/ChannelResolver.java     |  66 ---
 .../fineract/chat/archive/ChatArchiveApp.java      | 496 ---------------------
 .../apache/fineract/chat/archive/CursorStore.java  |  57 ---
 .../fineract/chat/archive/FileWriterUtil.java      |  42 --
 .../fineract/chat/archive/IndexRenderer.java       |  68 ---
 .../fineract/chat/archive/MarkdownRenderer.java    |  86 ----
 .../fineract/chat/archive/SlackApiClient.java      | 379 ----------------
 .../apache/fineract/chat/archive/SlackMessage.java |  31 --
 .../fineract/chat/archive/SlackTimestamp.java      |  54 ---
 .../chat/archive/UserDisplayNameResolver.java      |  51 ---
 .../fineract/chat/archive/ArchiveConfigTest.java   |  93 ----
 .../fineract/chat/archive/ChannelResolverTest.java |  55 ---
 .../chat/archive/UserDisplayNameResolverTest.java  |  50 ---
 27 files changed, 1 insertion(+), 2343 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 48426f9..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: Test
-
-on:
-  push:
-    branches: [main]
-  pull_request:
-    branches: [main]
-
-jobs:
-  build:
-    permissions:
-      contents: read
-    name: Run All Tests
-    runs-on: ubuntu-latest
-    timeout-minutes: 2
-    steps:
-      - uses: actions/checkout@v6
-      - uses: actions/setup-java@v5
-        with:
-          distribution: 'zulu'
-          java-version: '21'
-      - name: Run Tests
-        run: ./gradlew test
diff --git a/.github/workflows/update-archive.yml 
b/.github/workflows/update-archive.yml
deleted file mode 100644
index 4ed7ee0..0000000
--- a/.github/workflows/update-archive.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-name: Update chat archive
-
-on:
-  schedule:
-    - cron: '0 2 * * *'
-  workflow_dispatch:
-
-permissions:
-  contents: write
-
-jobs:
-  update:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-
-      - name: Setup Java
-        uses: actions/setup-java@v4
-        with:
-          distribution: 'temurin'
-          java-version: '21'
-          cache: 'gradle'
-
-      - name: Update archive
-        env:
-          SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
-        run: ./gradlew updateChatArchive
-
-      - name: Commit updates
-        run: |
-          if [ -d docs ]; then
-            git add docs
-          fi
-          if [ -d state ]; then
-            git add state
-          fi
-          if git diff --cached --quiet; then
-            echo "No changes to commit."
-            exit 0
-          fi
-          git config user.name "github-actions[bot]"
-          git config user.email "github-actions[bot]@users.noreply.github.com"
-          git commit -m "chore: update chat archive"
-          git push
diff --git a/.github/workflows/verify-commits.yml 
b/.github/workflows/verify-commits.yml
deleted file mode 100644
index a8c13fe..0000000
--- a/.github/workflows/verify-commits.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-name: Fineract Signed Commits Check
-
-on:
-  pull_request:
-    types: [opened, synchronize, reopened]
-
-permissions:
-  contents: read
-
-jobs:
-  verify-signatures:
-    name: Verify Commit Signatures
-    runs-on: ubuntu-latest
-    timeout-minutes: 1
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-
-      - name: Fetch base branch
-        run: git fetch origin ${{ github.base_ref }}
-
-      - name: Verify signatures
-        run: |
-          chmod +x scripts/verify-signed-commits.sh
-          ./scripts/verify-signed-commits.sh \
-            --base-ref origin/${{ github.base_ref }} \
-            --head-ref ${{ github.sha }} \
-            --strict
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 193f2b3..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-.gradle/
-build/
-out/
-.idea/
-*.iml
-*.log
diff --git a/Readme.md b/Readme.md
index 169bdd5..ac1f09f 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,49 +1 @@
-# Fineract Chat Archive
-
-A standalone tool that will archive public Slack messages for Apache Fineract 
into a static site.
-
-## Local run
-
-```
-./gradlew updateChatArchive
-```
-
-Configuration:
-- File: `config/archive.properties`
-- Key: `channels.allowlist` (comma-separated channel names, e.g. `#fineract`)
-- Key: `output.dir` (relative path for site output, default `docs`)
-- Key: `state.dir` (relative path for cursor state, default `state`)
-- Key: `fetch.lookback.days` (how many days to re-fetch, default `1`)
-
-Output:
-- Daily pages: `docs/daily/<channel>/<YYYY-MM-DD>.md`
-- Channel index: `docs/daily/<channel>/index.md`
-- Global index: `docs/index.md`
-- Thread replies are rendered below parent messages with a simple prefix.
-
-Environment:
-- `SLACK_TOKEN` (Slack Bot token)
-
-Slack app setup:
-1. Create a Slack app (from scratch) in the target workspace.
-2. Add a bot user.
-3. Add the required scopes listed below.
-4. Install the app to the workspace.
-5. Copy the Bot User OAuth Token (starts with `xoxb-`) into `SLACK_TOKEN`.
-
-Required Slack scopes:
-- `channels:read` (list public channels)
-- `channels:history` (read public channel history)
-- `users:read` (resolve user display names)
-Permalinks are resolved via `chat.getPermalink`. If Slack returns 
`missing_scope`,
-add the scope Slack reports and re-install the app.
-
-GitHub Pages:
-- The `docs/` directory is intended for publishing via GitHub Pages.
-
-## Automation
-
-GitHub Actions can run the archive update daily and commit new output.
-
-Setup:
-- Add repository secret `SLACK_TOKEN`
+moved to https://github.com/mifos/chat-archive
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index eeace21..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-plugins {
-    id 'java'
-}
-
-group = 'org.apache.fineract'
-version = '0.1.0-SNAPSHOT'
-
-repositories {
-    mavenCentral()
-}
-
-java {
-    toolchain {
-        languageVersion = JavaLanguageVersion.of(21)
-    }
-}
-
-dependencies {
-    implementation platform('com.fasterxml.jackson:jackson-bom:2.19.2')
-    implementation 'com.fasterxml.jackson.core:jackson-databind'
-
-    testImplementation platform('org.junit:junit-bom:5.11.3')
-    testImplementation 'org.junit.jupiter:junit-jupiter'
-}
-
-tasks.withType(Test).configureEach {
-    useJUnitPlatform()
-}
-
-tasks.register('updateChatArchive', JavaExec) {
-    group = 'application'
-    description = 'Fetch Slack messages and update the static archive.'
-    classpath = sourceSets.main.runtimeClasspath
-    mainClass.set('org.apache.fineract.chat.archive.ChatArchiveApp')
-}
diff --git a/config/archive.properties b/config/archive.properties
deleted file mode 100644
index f41ae71..0000000
--- a/config/archive.properties
+++ /dev/null
@@ -1,23 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-channels.allowlist=#fineract
-output.dir=docs
-state.dir=state
-fetch.lookback.days=1
-
diff --git a/gradle/wrapper/gradle-wrapper.jar 
b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index d64cd49..0000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties 
b/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index d4081da..0000000
--- a/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
deleted file mode 100755
index 1aa94a4..0000000
--- a/gradlew
+++ /dev/null
@@ -1,249 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-#   Gradle start up script for POSIX generated by Gradle.
-#
-#   Important for running:
-#
-#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-#       noncompliant, but you have some other compliant shell such as ksh or
-#       bash, then to run this script, type that shell name before the whole
-#       command line, like:
-#
-#           ksh Gradle
-#
-#       Busybox and similar reduced shells will NOT work, because this script
-#       requires all of these POSIX shell features:
-#         * functions;
-#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-#         * compound commands having a testable exit status, especially «case»;
-#         * various built-in commands including «command», «set», and «ulimit».
-#
-#   Important for patching:
-#
-#   (2) This script targets any POSIX shell, so it avoids extensions provided
-#       by Bash, Ksh, etc; in particular arrays are avoided.
-#
-#       The "traditional" practice of packing multiple parameters into a
-#       space-separated string is a well documented source of bugs and security
-#       problems, so this is (mostly) avoided, by progressively accumulating
-#       options in "$@", and eventually passing that to Java.
-#
-#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-#       see the in-line comments for details.
-#
-#       There are tweaks for specific operating systems such as AIX, CygWin,
-#       Darwin, MinGW, and NonStop.
-#
-#   (3) This script is generated from the Groovy template
-#       
https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-#       within the Gradle project.
-#
-#       You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
-    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no 
leading path
-    [ -h "$app_path" ]
-do
-    ls=$( ls -ld "$app_path" )
-    link=${ls#*' -> '}
-    case $link in             #(
-      /*)   app_path=$link ;; #(
-      *)    app_path=$APP_HOME$link ;;
-    esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set 
(https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
-    echo "$*"
-} >&2
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in                #(
-  CYGWIN* )         cygwin=true  ;; #(
-  Darwin* )         darwin=true  ;; #(
-  MSYS* | MINGW* )  msys=true    ;; #(
-  NONSTOP* )        nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD=$JAVA_HOME/jre/sh/java
-    else
-        JAVACMD=$JAVA_HOME/bin/java
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD=java
-    if ! command -v java >/dev/null 2>&1
-    then
-        die "ERROR: JAVA_HOME is not set and no 'java' command could be found 
in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
-    case $MAX_FD in #(
-      max*)
-        # In POSIX sh, ulimit -H is undefined. That's why the result is 
checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        MAX_FD=$( ulimit -H -n ) ||
-            warn "Could not query maximum file descriptor limit"
-    esac
-    case $MAX_FD in  #(
-      '' | soft) :;; #(
-      *)
-        # In POSIX sh, ulimit -n is undefined. That's why the result is 
checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        ulimit -n "$MAX_FD" ||
-            warn "Could not set maximum file descriptor limit to $MAX_FD"
-    esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-#   * args from the command line
-#   * the main class name
-#   * -classpath
-#   * -D...appname settings
-#   * --module-path (only if needed)
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
-    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
-    JAVACMD=$( cygpath --unix "$JAVACMD" )
-
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    for arg do
-        if
-            case $arg in                                #(
-              -*)   false ;;                            # don't mess with 
options #(
-              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX 
filepath
-                    [ -e "$t" ] ;;                      #(
-              *)    false ;;
-            esac
-        then
-            arg=$( cygpath --path --ignore --mixed "$arg" )
-        fi
-        # Roll the args list around exactly as many times as the number of
-        # args, so each arg winds up back in the position where it started, but
-        # possibly modified.
-        #
-        # NB: a `for` loop captures its iteration list before it begins, so
-        # changing the positional parameters here affects neither the number of
-        # iterations, nor the values presented in `arg`.
-        shift                   # remove old arg
-        set -- "$@" "$arg"      # push replacement arg
-    done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to 
pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not 
allowed to contain shell fragments,
-#     and any embedded shellness will be escaped.
-#   * For example: A user cannot expect ${Hostname} to be expanded, as it is 
an environment variable and will be
-#     treated as '${Hostname}' itself on the command line.
-
-set -- \
-        "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -classpath "$CLASSPATH" \
-        org.gradle.wrapper.GradleWrapperMain \
-        "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
-    die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes 
removed.
-#
-# In Bash we could simply go:
-#
-#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-#   set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
-        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
-        xargs -n1 |
-        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
-        tr '\n' ' '
-    )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
deleted file mode 100644
index 25da30d..0000000
--- a/gradlew.bat
+++ /dev/null
@@ -1,92 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS 
to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your 
PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% 
"-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" 
org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code 
instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/scripts/verify-signed-commits.sh b/scripts/verify-signed-commits.sh
deleted file mode 100755
index ee3689a..0000000
--- a/scripts/verify-signed-commits.sh
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/bin/bash
-
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# Usage: ./scripts/verify-signed-commits.sh [--base-ref <ref>] [--head-ref 
<ref>] [--strict] [--help]
-set -e
-
-BASE_REF="origin/main"
-HEAD_REF="HEAD"
-STRICT_MODE=false
-
-show_help() {
-    cat << 'EOF'
-Usage: ./scripts/verify-signed-commits.sh [OPTIONS]
-
-Options:
-  --base-ref <ref>   Base reference (default: origin/develop)
-  --head-ref <ref>   Head reference (default: HEAD)
-  --strict           Exit with error if unsigned commits found
-  --help             Show this help
-EOF
-}
-
-while [[ $# -gt 0 ]]; do
-    case $1 in
-        --base-ref) BASE_REF="$2"; shift 2 ;;
-        --head-ref) HEAD_REF="$2"; shift 2 ;;
-        --strict) STRICT_MODE=true; shift ;;
-        --help) show_help; exit 0 ;;
-        *) echo "Unknown option: $1"; show_help; exit 1 ;;
-    esac
-done
-
-MERGE_BASE=$(git merge-base "$BASE_REF" "$HEAD_REF" 2>/dev/null || echo "")
-if [ -z "$MERGE_BASE" ]; then
-    COMMIT_RANGE="$HEAD_REF~10..$HEAD_REF"
-else
-    COMMIT_RANGE="$MERGE_BASE..$HEAD_REF"
-fi
-
-echo "Verifying commit signatures in range: $COMMIT_RANGE"
-
-COMMITS=$(git log --format="%H%x1f%G?%x1f%an%x1f%s" "$COMMIT_RANGE" 
2>/dev/null || echo "")
-if [ -z "$COMMITS" ]; then
-    echo "No commits to verify."
-    exit 0
-fi
-
-UNSIGNED_COUNT=0
-
-while IFS=$'\x1f' read -r HASH SIG_STATUS AUTHOR SUBJECT; do
-    [ -z "$HASH" ] && continue
-    SHORT_HASH="${HASH:0:7}"
-
-    case "$SIG_STATUS" in
-        N)
-            UNSIGNED_COUNT=$((UNSIGNED_COUNT + 1))
-            if [ -n "$GITHUB_ACTIONS" ]; then
-                echo "::error title=Unsigned Commit::Commit $SHORT_HASH by 
$AUTHOR is not signed."
-            else
-                echo "❌ Unsigned: $SHORT_HASH - $SUBJECT ($AUTHOR)"
-            fi
-            ;;
-        *)
-            echo "✅ Signed: $SHORT_HASH - $SUBJECT"
-            ;;
-    esac
-done <<< "$COMMITS"
-
-echo ""
-echo "Summary: $UNSIGNED_COUNT unsigned commit(s) found."
-
-if [ "$STRICT_MODE" = true ] && [ "$UNSIGNED_COUNT" -gt 0 ]; then
-    if [ -n "$GITHUB_ACTIONS" ]; then
-        echo "::error::$UNSIGNED_COUNT unsigned commit(s). See 
CONTRIBUTING.md#signing-your-commits"
-    else
-        echo "❌ $UNSIGNED_COUNT unsigned commit(s). See 
CONTRIBUTING.md#signing-your-commits"
-    fi
-    exit 1
-fi
-
-exit 0
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index 69f82d6..0000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'fineract-chat-archive'
diff --git a/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java 
b/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java
deleted file mode 100644
index cbd62d5..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.Properties;
-
-final class ArchiveConfig {
-
-    static final String CHANNELS_ALLOWLIST_KEY = "channels.allowlist";
-    static final String OUTPUT_DIR_KEY = "output.dir";
-    static final String STATE_DIR_KEY = "state.dir";
-    static final String LOOKBACK_DAYS_KEY = "fetch.lookback.days";
-    static final int DEFAULT_LOOKBACK_DAYS = 1;
-
-    private final List<String> channelAllowlist;
-    private final Path outputDir;
-    private final Path stateDir;
-    private final int lookbackDays;
-
-    private ArchiveConfig(List<String> channelAllowlist, Path outputDir, Path 
stateDir,
-            int lookbackDays) {
-        this.channelAllowlist = List.copyOf(channelAllowlist);
-        this.outputDir = outputDir;
-        this.stateDir = stateDir;
-        this.lookbackDays = lookbackDays;
-    }
-
-    static Optional<ArchiveConfig> load(Path configPath) throws IOException {
-        if (!Files.exists(configPath)) {
-            return Optional.empty();
-        }
-
-        Properties properties = new Properties();
-        try (InputStream inputStream = Files.newInputStream(configPath)) {
-            properties.load(inputStream);
-        }
-
-        String allowlist = properties.getProperty(CHANNELS_ALLOWLIST_KEY, "");
-        List<String> channels = parseAllowlist(allowlist);
-        if (channels.isEmpty()) {
-            return Optional.empty();
-        }
-
-        String outputDirValue = properties.getProperty(OUTPUT_DIR_KEY, 
"docs").trim();
-        Path outputDir = Path.of(outputDirValue);
-        String stateDirValue = properties.getProperty(STATE_DIR_KEY, 
"state").trim();
-        Path stateDir = Path.of(stateDirValue);
-        int lookbackDays = 
parseLookbackDays(properties.getProperty(LOOKBACK_DAYS_KEY));
-        return Optional.of(new ArchiveConfig(channels, outputDir, stateDir, 
lookbackDays));
-    }
-
-    List<String> channelAllowlist() {
-        return channelAllowlist;
-    }
-
-    Path outputDir() {
-        return outputDir;
-    }
-
-    Path stateDir() {
-        return stateDir;
-    }
-
-    int lookbackDays() {
-        return lookbackDays;
-    }
-
-    private static List<String> parseAllowlist(String value) {
-        if (value == null || value.isBlank()) {
-            return List.of();
-        }
-
-        return Arrays.stream(value.split(","))
-                .map(String::trim)
-                .filter(token -> !token.isEmpty())
-                .map(ArchiveConfig::stripLeadingHash)
-                .filter(token -> !token.isEmpty())
-                .toList();
-    }
-
-    private static String stripLeadingHash(String value) {
-        if (value.startsWith("#")) {
-            return value.substring(1).trim();
-        }
-        return value;
-    }
-
-    private static int parseLookbackDays(String value) {
-        if (value == null || value.isBlank()) {
-            return DEFAULT_LOOKBACK_DAYS;
-        }
-        try {
-            int parsed = Integer.parseInt(value.trim());
-            return parsed > 0 ? parsed : DEFAULT_LOOKBACK_DAYS;
-        } catch (NumberFormatException ex) {
-            return DEFAULT_LOOKBACK_DAYS;
-        }
-    }
-}
-
diff --git 
a/src/main/java/org/apache/fineract/chat/archive/ChannelResolver.java 
b/src/main/java/org/apache/fineract/chat/archive/ChannelResolver.java
deleted file mode 100644
index 4f9c3a7..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/ChannelResolver.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-final class ChannelResolver {
-
-    private ChannelResolver() {}
-
-    static ChannelResolution resolve(List<String> allowlist,
-            List<SlackApiClient.SlackChannel> channels) {
-        Map<String, SlackApiClient.SlackChannel> channelsByName = new 
HashMap<>();
-        for (SlackApiClient.SlackChannel channel : channels) {
-            String normalizedName = normalize(channel.name());
-            if (!normalizedName.isEmpty()) {
-                channelsByName.putIfAbsent(normalizedName, channel);
-            }
-        }
-
-        List<SlackApiClient.SlackChannel> resolved = new ArrayList<>();
-        List<String> missing = new ArrayList<>();
-        for (String allowed : allowlist) {
-            String normalizedAllowed = normalize(allowed);
-            SlackApiClient.SlackChannel channel = 
channelsByName.get(normalizedAllowed);
-            if (channel == null) {
-                missing.add(allowed);
-            } else {
-                resolved.add(channel);
-            }
-        }
-
-        return new ChannelResolution(List.copyOf(resolved), 
List.copyOf(missing));
-    }
-
-    private static String normalize(String value) {
-        if (value == null) {
-            return "";
-        }
-        return value.trim().toLowerCase(Locale.ROOT);
-    }
-
-    record ChannelResolution(List<SlackApiClient.SlackChannel> resolved,
-            List<String> missing) {
-    }
-}
diff --git a/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java 
b/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java
deleted file mode 100644
index 30e64b6..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java
+++ /dev/null
@@ -1,496 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.TreeMap;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public final class ChatArchiveApp {
-
-    static final String SLACK_TOKEN_ENV = "SLACK_TOKEN";
-
-    private static final Logger LOG = 
Logger.getLogger(ChatArchiveApp.class.getName());
-    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter
-            .ofPattern("HH:mm:ss");
-
-    private ChatArchiveApp() {}
-
-    public static void main(String[] args) {
-        Optional<String> slackToken = readEnv(SLACK_TOKEN_ENV);
-        if (slackToken.isEmpty()) {
-            LOG.info("SLACK_TOKEN is not set. Skipping archive update.");
-            return;
-        }
-
-        Path configPath = Path.of("config", "archive.properties");
-        Optional<ArchiveConfig> config;
-        try {
-            config = ArchiveConfig.load(configPath);
-        } catch (IOException ex) {
-            LOG.log(Level.SEVERE, "Failed to read config at " + configPath + 
".", ex);
-            return;
-        }
-
-        if (config.isEmpty()) {
-            LOG.info("Config file missing or channel allowlist empty. Skipping 
archive update.");
-            return;
-        }
-
-        ArchiveConfig archiveConfig = config.get();
-        LOG.info("Loaded config for " + archiveConfig.channelAllowlist().size()
-                + " channel(s).");
-
-        SlackApiClient slackApiClient = new SlackApiClient();
-        SlackApiClient.AuthTestResponse authResponse;
-        try {
-            authResponse = slackApiClient.authTest(slackToken.get());
-        } catch (IOException ex) {
-            LOG.log(Level.SEVERE, "Slack auth.test call failed.", ex);
-            return;
-        } catch (InterruptedException ex) {
-            Thread.currentThread().interrupt();
-            LOG.log(Level.SEVERE, "Slack auth.test call interrupted.", ex);
-            return;
-        }
-
-        if (!authResponse.ok()) {
-            LOG.warning("Slack auth.test failed: " + authResponse.error());
-            return;
-        }
-
-        LOG.info("Slack auth.test succeeded for team " + authResponse.team() + 
".");
-
-        SlackApiClient.ConversationsListResponse channelsResponse;
-        try {
-            channelsResponse = 
slackApiClient.listPublicChannels(slackToken.get());
-        } catch (IOException ex) {
-            LOG.log(Level.SEVERE, "Slack conversations.list call failed.", ex);
-            return;
-        } catch (InterruptedException ex) {
-            Thread.currentThread().interrupt();
-            LOG.log(Level.SEVERE, "Slack conversations.list call 
interrupted.", ex);
-            return;
-        }
-
-        if (!channelsResponse.ok()) {
-            LOG.warning("Slack conversations.list failed: " + 
channelsResponse.error());
-            return;
-        }
-
-        List<SlackApiClient.SlackChannel> channels = 
channelsResponse.channels();
-        ChannelResolver.ChannelResolution resolution = ChannelResolver.resolve(
-                archiveConfig.channelAllowlist(), channels);
-
-        if (!resolution.missing().isEmpty()) {
-            LOG.warning("Allowlisted channel(s) not found: "
-                    + String.join(", ", resolution.missing()));
-        }
-
-        if (resolution.resolved().isEmpty()) {
-            LOG.warning("No allowlisted channels resolved. Skipping archive 
update.");
-            return;
-        }
-
-        LOG.info("Resolved " + resolution.resolved().size() + " channel(s).");
-
-        Instant windowStart = Instant.now()
-                .minus(Duration.ofDays(archiveConfig.lookbackDays()));
-        String windowOldest = 
SlackTimestamp.formatEpochSecond(windowStart.getEpochSecond());
-
-        CursorStore cursorStore = new CursorStore(archiveConfig.stateDir());
-        CursorStore.CursorState cursorState = loadCursorState(cursorStore);
-        Map<String, String> cursors = new HashMap<>(cursorState.channels());
-
-        Path dailyRoot = archiveConfig.outputDir().resolve("daily");
-        Map<String, String> permalinkCache = new HashMap<>();
-        Map<String, String> userCache = new HashMap<>();
-        Map<String, List<SlackMessage>> threadRepliesCache = new HashMap<>();
-        boolean anyRendered = false;
-
-        for (SlackApiClient.SlackChannel channel : resolution.resolved()) {
-            String channelId = channel.id();
-            String oldest = determineOldestTs(windowStart, windowOldest,
-                    cursors.get(channelId));
-            SlackApiClient.ConversationsHistoryResponse historyResponse;
-            try {
-                historyResponse = 
slackApiClient.listChannelMessages(slackToken.get(), channelId,
-                        oldest);
-            } catch (IOException ex) {
-                LOG.log(Level.SEVERE, "Slack conversations.history call failed 
for channel "
-                        + channel.name() + ".", ex);
-                continue;
-            } catch (InterruptedException ex) {
-                Thread.currentThread().interrupt();
-                LOG.log(Level.SEVERE, "Slack conversations.history call 
interrupted for channel "
-                        + channel.name() + ".", ex);
-                continue;
-            }
-
-            if (!historyResponse.ok()) {
-                LOG.warning("Slack conversations.history failed for channel " 
+ channel.name()
-                        + ": " + historyResponse.error());
-                continue;
-            }
-
-            List<SlackMessage> messages = new 
ArrayList<>(historyResponse.messages());
-            messages.sort((left, right) -> SlackTimestamp.compare(left.ts(), 
right.ts()));
-            String latestTs = updateCursor(cursors.get(channelId), messages);
-            if (latestTs != null) {
-                cursors.put(channelId, latestTs);
-            }
-
-            Map<LocalDate, List<SlackMessage>> grouped = groupByDate(messages);
-            for (Map.Entry<LocalDate, List<SlackMessage>> entry : 
grouped.entrySet()) {
-                LocalDate date = entry.getKey();
-                List<MarkdownRenderer.Row> rows = toRows(entry.getValue(), 
channelId,
-                        slackToken.get(), slackApiClient, permalinkCache, 
userCache,
-                        threadRepliesCache);
-                String page = MarkdownRenderer.renderDailyPage(channel.name(), 
date, rows);
-                Path pagePath = dailyRoot.resolve(channel.name()).resolve(date 
+ ".md");
-                try {
-                    boolean changed = FileWriterUtil.writeIfChanged(pagePath, 
page);
-                    anyRendered = anyRendered || changed;
-                } catch (IOException ex) {
-                    LOG.log(Level.SEVERE, "Failed to write archive for channel 
"
-                            + channel.name() + " on " + date + ".", ex);
-                }
-            }
-        }
-
-        if (saveCursorState(cursorStore, cursors)) {
-            anyRendered = true;
-        }
-
-        if (renderIndexes(dailyRoot)) {
-            anyRendered = true;
-        }
-
-        if (!anyRendered) {
-            LOG.info("No changes detected. Archive output unchanged.");
-        }
-    }
-
-    static Optional<String> readEnv(String name) {
-        String value = System.getenv(name);
-        if (value == null || value.isBlank()) {
-            return Optional.empty();
-        }
-        return Optional.of(value);
-    }
-
-    private static CursorStore.CursorState loadCursorState(CursorStore 
cursorStore) {
-        try {
-            return 
cursorStore.load().orElseGet(CursorStore.CursorState::empty);
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Failed to read cursor state. Starting 
fresh.", ex);
-            return CursorStore.CursorState.empty();
-        }
-    }
-
-    private static boolean saveCursorState(CursorStore cursorStore, 
Map<String, String> cursors) {
-        try {
-            cursorStore.save(new CursorStore.CursorState(Map.copyOf(cursors)));
-            return true;
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Failed to write cursor state.", ex);
-            return false;
-        }
-    }
-
-    private static String determineOldestTs(Instant windowStart, String 
windowOldest,
-            String cursorTs) {
-        if (cursorTs == null || cursorTs.isBlank()) {
-            return windowOldest;
-        }
-        Instant cursorInstant = SlackTimestamp.toInstant(cursorTs);
-        if (cursorInstant.isBefore(windowStart)) {
-            return cursorTs;
-        }
-        return windowOldest;
-    }
-
-    private static String updateCursor(String current, List<SlackMessage> 
messages) {
-        String latest = current;
-        for (SlackMessage message : messages) {
-            if (message.ts() == null) {
-                continue;
-            }
-            if (latest == null || SlackTimestamp.compare(message.ts(), latest) 
> 0) {
-                latest = message.ts();
-            }
-        }
-        return latest;
-    }
-
-    private static Map<LocalDate, List<SlackMessage>> 
groupByDate(List<SlackMessage> messages) {
-        Map<LocalDate, List<SlackMessage>> grouped = new TreeMap<>();
-        Set<String> parentSet = new HashSet<>();
-        for (SlackMessage message : messages) {
-            if (message.ts() == null) {
-                continue;
-            }
-            if (message.threadTs() == null || 
message.threadTs().equals(message.ts())) {
-                parentSet.add(message.ts());
-            }
-        }
-        for (SlackMessage message : messages) {
-            if (message.ts() == null) {
-                continue;
-            }
-            if (isReply(message) && parentSet.contains(message.threadTs())) {
-                continue;
-            }
-            Instant instant = SlackTimestamp.toInstant(message.ts());
-            LocalDate date = instant.atZone(ZoneOffset.UTC).toLocalDate();
-            grouped.computeIfAbsent(date, key -> new 
ArrayList<>()).add(message);
-        }
-        return grouped;
-    }
-
-    private static List<MarkdownRenderer.Row> toRows(List<SlackMessage> 
messages, String channelId,
-            String token, SlackApiClient slackApiClient, Map<String, String> 
permalinkCache,
-            Map<String, String> userCache, Map<String, List<SlackMessage>> 
threadRepliesCache) {
-        List<MarkdownRenderer.Row> rows = new ArrayList<>();
-        Map<String, List<SlackMessage>> repliesByParent = 
collectReplies(messages);
-        Set<String> parentSet = collectParentIds(messages);
-        for (SlackMessage message : messages) {
-            if (message.ts() == null) {
-                continue;
-            }
-            if (isReply(message)) {
-                if (parentSet.contains(message.threadTs())) {
-                    continue;
-                }
-                rows.add(toRow(message, channelId, token, slackApiClient, 
permalinkCache,
-                        userCache, "-> "));
-                continue;
-            }
-            rows.add(toRow(message, channelId, token, slackApiClient, 
permalinkCache, userCache,
-                    ""));
-            if (message.threadTs() != null && 
message.threadTs().equals(message.ts())) {
-                List<SlackMessage> replies = resolveThreadReplies(channelId, 
message.threadTs(),
-                        repliesByParent, threadRepliesCache, slackApiClient, 
token);
-                for (SlackMessage reply : replies) {
-                    rows.add(toRow(reply, channelId, token, slackApiClient, 
permalinkCache,
-                            userCache, "-> "));
-                }
-            }
-        }
-        return rows;
-    }
-
-    private static MarkdownRenderer.Row toRow(SlackMessage message, String 
channelId, String token,
-            SlackApiClient slackApiClient, Map<String, String> permalinkCache,
-            Map<String, String> userCache, String prefix) {
-        Instant instant = SlackTimestamp.toInstant(message.ts());
-        String time = TIME_FORMATTER.format(instant.atZone(ZoneOffset.UTC));
-        String user = resolveUser(message, token, slackApiClient, userCache);
-        String text = message.text() == null ? "" : message.text();
-        String permalink = resolvePermalink(channelId, message.ts(), token, 
slackApiClient,
-                permalinkCache);
-        return new MarkdownRenderer.Row(time, user, prefix + text, permalink);
-    }
-
-    private static boolean isReply(SlackMessage message) {
-        return message.threadTs() != null && 
!message.threadTs().equals(message.ts());
-    }
-
-    private static Set<String> collectParentIds(List<SlackMessage> messages) {
-        Set<String> parentSet = new HashSet<>();
-        for (SlackMessage message : messages) {
-            if (message.ts() == null) {
-                continue;
-            }
-            if (message.threadTs() == null || 
message.threadTs().equals(message.ts())) {
-                parentSet.add(message.ts());
-            }
-        }
-        return parentSet;
-    }
-
-    private static Map<String, List<SlackMessage>> 
collectReplies(List<SlackMessage> messages) {
-        Map<String, List<SlackMessage>> repliesByParent = new HashMap<>();
-        for (SlackMessage message : messages) {
-            if (message.ts() == null || message.threadTs() == null
-                    || message.threadTs().equals(message.ts())) {
-                continue;
-            }
-            repliesByParent.computeIfAbsent(message.threadTs(), key -> new 
ArrayList<>())
-                    .add(message);
-        }
-        return repliesByParent;
-    }
-
-    private static List<SlackMessage> resolveThreadReplies(String channelId, 
String threadTs,
-            Map<String, List<SlackMessage>> repliesByParent,
-            Map<String, List<SlackMessage>> repliesCache, SlackApiClient 
slackApiClient,
-            String token) {
-        if (repliesCache.containsKey(threadTs)) {
-            return repliesCache.get(threadTs);
-        }
-
-        List<SlackMessage> replies = new 
ArrayList<>(repliesByParent.getOrDefault(threadTs,
-                List.of()));
-        Set<String> replyIds = new HashSet<>();
-        for (SlackMessage message : replies) {
-            if (message.ts() != null) {
-                replyIds.add(message.ts());
-            }
-        }
-
-        SlackApiClient.ConversationsRepliesResponse response;
-        try {
-            response = slackApiClient.listThreadReplies(token, channelId, 
threadTs);
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Slack conversations.replies call failed.", 
ex);
-            repliesCache.put(threadTs, List.copyOf(replies));
-            return replies;
-        } catch (InterruptedException ex) {
-            Thread.currentThread().interrupt();
-            LOG.log(Level.WARNING, "Slack conversations.replies call 
interrupted.", ex);
-            repliesCache.put(threadTs, List.copyOf(replies));
-            return replies;
-        }
-
-        if (!response.ok()) {
-            LOG.warning("Slack conversations.replies failed: " + 
response.error());
-            repliesCache.put(threadTs, List.copyOf(replies));
-            return replies;
-        }
-
-        for (SlackMessage message : response.messages()) {
-            if (message.ts() == null || message.ts().equals(threadTs)) {
-                continue;
-            }
-            if (replyIds.add(message.ts())) {
-                replies.add(message);
-            }
-        }
-        replies.sort((left, right) -> SlackTimestamp.compare(left.ts(), 
right.ts()));
-        List<SlackMessage> merged = List.copyOf(replies);
-        repliesCache.put(threadTs, merged);
-        return merged;
-    }
-
-    private static String resolveUser(SlackMessage message, String token,
-            SlackApiClient slackApiClient, Map<String, String> userCache) {
-        if (message.user() != null && !message.user().isBlank()) {
-            return resolveUserDisplayName(message.user(), token, 
slackApiClient, userCache);
-        }
-        if (message.botId() != null && !message.botId().isBlank()) {
-            return "bot:" + message.botId();
-        }
-        return "unknown";
-    }
-
-    private static String resolveUserDisplayName(String userId, String token,
-            SlackApiClient slackApiClient, Map<String, String> userCache) {
-        if (userCache.containsKey(userId)) {
-            return userCache.get(userId);
-        }
-        SlackApiClient.UserInfoResponse response;
-        try {
-            response = slackApiClient.getUserInfo(token, userId);
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Slack users.info call failed.", ex);
-            userCache.put(userId, userId);
-            return userId;
-        } catch (InterruptedException ex) {
-            Thread.currentThread().interrupt();
-            LOG.log(Level.WARNING, "Slack users.info call interrupted.", ex);
-            userCache.put(userId, userId);
-            return userId;
-        }
-
-        if (!response.ok() || response.user() == null) {
-            userCache.put(userId, userId);
-            return userId;
-        }
-
-        String displayName = UserDisplayNameResolver.resolve(response.user());
-        userCache.put(userId, displayName);
-        return displayName;
-    }
-
-    private static String resolvePermalink(String channelId, String messageTs, 
String token,
-            SlackApiClient slackApiClient, Map<String, String> permalinkCache) 
{
-        if (messageTs == null || messageTs.isBlank()) {
-            return null;
-        }
-        String cacheKey = channelId + ":" + messageTs;
-        if (permalinkCache.containsKey(cacheKey)) {
-            return permalinkCache.get(cacheKey);
-        }
-        SlackApiClient.PermalinkResponse response;
-        try {
-            response = slackApiClient.getPermalink(token, channelId, 
messageTs);
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Slack chat.getPermalink call failed.", ex);
-            permalinkCache.put(cacheKey, null);
-            return null;
-        } catch (InterruptedException ex) {
-            Thread.currentThread().interrupt();
-            LOG.log(Level.WARNING, "Slack chat.getPermalink call 
interrupted.", ex);
-            permalinkCache.put(cacheKey, null);
-            return null;
-        }
-        if (!response.ok()) {
-            LOG.warning("Slack chat.getPermalink failed: " + response.error());
-            permalinkCache.put(cacheKey, null);
-            return null;
-        }
-        permalinkCache.put(cacheKey, response.permalink());
-        return response.permalink();
-    }
-
-    private static boolean renderIndexes(Path dailyRoot) {
-        boolean changed = false;
-        try {
-            List<String> channels = IndexRenderer.listChannels(dailyRoot);
-            for (String channel : channels) {
-                List<LocalDate> dates = 
IndexRenderer.listDates(dailyRoot.resolve(channel));
-                String index = MarkdownRenderer.renderChannelIndex(channel, 
dates);
-                Path indexPath = 
dailyRoot.resolve(channel).resolve("index.md");
-                changed = FileWriterUtil.writeIfChanged(indexPath, index) || 
changed;
-            }
-            String globalIndex = MarkdownRenderer.renderGlobalIndex(channels);
-            Path globalPath = dailyRoot.getParent().resolve("index.md");
-            changed = FileWriterUtil.writeIfChanged(globalPath, globalIndex) 
|| changed;
-        } catch (IOException ex) {
-            LOG.log(Level.WARNING, "Failed to write index files.", ex);
-        }
-        return changed;
-    }
-}
-
diff --git a/src/main/java/org/apache/fineract/chat/archive/CursorStore.java 
b/src/main/java/org/apache/fineract/chat/archive/CursorStore.java
deleted file mode 100644
index 044aff9..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/CursorStore.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.Optional;
-
-final class CursorStore {
-
-    private static final String CURSOR_FILE_NAME = "cursor.json";
-
-    private final Path cursorFile;
-    private final ObjectMapper objectMapper;
-
-    CursorStore(Path stateDir) {
-        this.cursorFile = stateDir.resolve(CURSOR_FILE_NAME);
-        this.objectMapper = new ObjectMapper();
-    }
-
-    Optional<CursorState> load() throws IOException {
-        if (!Files.exists(cursorFile)) {
-            return Optional.empty();
-        }
-        return Optional.of(objectMapper.readValue(cursorFile.toFile(), 
CursorState.class));
-    }
-
-    void save(CursorState state) throws IOException {
-        Files.createDirectories(cursorFile.getParent());
-        
objectMapper.writerWithDefaultPrettyPrinter().writeValue(cursorFile.toFile(), 
state);
-    }
-
-    record CursorState(Map<String, String> channels) {
-        static CursorState empty() {
-            return new CursorState(Map.of());
-        }
-    }
-}
diff --git a/src/main/java/org/apache/fineract/chat/archive/FileWriterUtil.java 
b/src/main/java/org/apache/fineract/chat/archive/FileWriterUtil.java
deleted file mode 100644
index 9b31861..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/FileWriterUtil.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-final class FileWriterUtil {
-
-    private FileWriterUtil() {}
-
-    static boolean writeIfChanged(Path path, String content) throws 
IOException {
-        String existing = null;
-        if (Files.exists(path)) {
-            existing = Files.readString(path, StandardCharsets.UTF_8);
-        }
-        if (content.equals(existing)) {
-            return false;
-        }
-        Files.createDirectories(path.getParent());
-        Files.writeString(path, content, StandardCharsets.UTF_8);
-        return true;
-    }
-}
diff --git a/src/main/java/org/apache/fineract/chat/archive/IndexRenderer.java 
b/src/main/java/org/apache/fineract/chat/archive/IndexRenderer.java
deleted file mode 100644
index 65a1cca..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/IndexRenderer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.LocalDate;
-import java.util.Comparator;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-final class IndexRenderer {
-
-    private IndexRenderer() {}
-
-    static List<String> listChannels(Path dailyRoot) throws IOException {
-        if (!Files.exists(dailyRoot)) {
-            return List.of();
-        }
-        try (Stream<Path> stream = Files.list(dailyRoot)) {
-            return stream.filter(Files::isDirectory)
-                    .map(path -> path.getFileName().toString())
-                    .sorted(String.CASE_INSENSITIVE_ORDER)
-                    .toList();
-        }
-    }
-
-    static List<LocalDate> listDates(Path channelDir) throws IOException {
-        if (!Files.exists(channelDir)) {
-            return List.of();
-        }
-        try (Stream<Path> stream = Files.list(channelDir)) {
-            return stream.filter(path -> 
path.getFileName().toString().endsWith(".md"))
-                    .filter(path -> 
!path.getFileName().toString().equalsIgnoreCase("index.md"))
-                    .map(path -> path.getFileName().toString().replace(".md", 
""))
-                    .map(IndexRenderer::parseDate)
-                    .filter(date -> date != null)
-                    .sorted(Comparator.reverseOrder())
-                    .collect(Collectors.toList());
-        }
-    }
-
-    private static LocalDate parseDate(String value) {
-        try {
-            return LocalDate.parse(value);
-        } catch (Exception ex) {
-            return null;
-        }
-    }
-}
diff --git 
a/src/main/java/org/apache/fineract/chat/archive/MarkdownRenderer.java 
b/src/main/java/org/apache/fineract/chat/archive/MarkdownRenderer.java
deleted file mode 100644
index 52b4fb1..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/MarkdownRenderer.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.time.LocalDate;
-import java.util.List;
-
-class MarkdownRenderer {
-
-    private MarkdownRenderer() {}
-
-    static String renderDailyPage(String channelName, LocalDate date, 
List<Row> rows) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("---\n");
-        builder.append("title: \"#").append(channelName).append(" 
").append(date).append("\"\n");
-        builder.append("date: ").append(date).append('\n');
-        builder.append("channel: ").append(channelName).append('\n');
-        builder.append("---\n\n");
-
-        for (Row row : rows) {
-            builder.append(formatTimeCell(row)).append(" - ")
-                    .append(normalize(row.user())).append(" - ")
-                    .append(normalize(row.message())).append("\n");
-        }
-        return builder.toString();
-    }
-
-    static String renderChannelIndex(String channelName, List<LocalDate> 
dates) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("---\n");
-        builder.append("title: \"#").append(channelName).append("\"\n");
-        builder.append("channel: ").append(channelName).append('\n');
-        builder.append("---\n\n");
-        builder.append("## Days\n\n");
-        for (LocalDate date : dates) {
-            builder.append("- 
[").append(date).append("](").append(date).append(".md)\n");
-        }
-        return builder.toString();
-    }
-
-    static String renderGlobalIndex(List<String> channels) {
-        StringBuilder builder = new StringBuilder();
-        builder.append("---\n");
-        builder.append("title: \"Fineract Chat Archive\"\n");
-        builder.append("---\n\n");
-        builder.append("## Channels\n\n");
-        for (String channel : channels) {
-            builder.append("- [#").append(channel).append("](daily/")
-                    .append(channel).append("/index.md)\n");
-        }
-        return builder.toString();
-    }
-
-    private static String formatTimeCell(Row row) {
-        if (row.permalink() == null || row.permalink().isBlank()) {
-            return row.time();
-        }
-        return "[" + row.time() + "](" + row.permalink() + ")";
-    }
-
-    private static String normalize(String value) {
-        if (value == null) {
-            return "";
-        }
-        return value.replace("\r\n", "\n").replace("\r", "\n");
-    }
-
-    record Row(String time, String user, String message, String permalink) {
-    }
-}
diff --git a/src/main/java/org/apache/fineract/chat/archive/SlackApiClient.java 
b/src/main/java/org/apache/fineract/chat/archive/SlackApiClient.java
deleted file mode 100644
index cfdbfaf..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/SlackApiClient.java
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URLEncoder;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-class SlackApiClient {
-
-    private static final URI AUTH_TEST_URI = 
URI.create("https://slack.com/api/auth.test";);
-    private static final String CONVERSATIONS_LIST_URL = 
"https://slack.com/api/conversations.list";;
-    private static final String CONVERSATIONS_HISTORY_URL =
-            "https://slack.com/api/conversations.history";;
-    private static final String CONVERSATIONS_REPLIES_URL =
-            "https://slack.com/api/conversations.replies";;
-    private static final String CHAT_PERMALINK_URL = 
"https://slack.com/api/chat.getPermalink";;
-    private static final String USERS_INFO_URL = 
"https://slack.com/api/users.info";;
-    private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10);
-    private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20);
-    private static final int CONVERSATIONS_PAGE_SIZE = 200;
-    private static final int HISTORY_PAGE_SIZE = 200;
-
-    private final HttpClient httpClient;
-    private final ObjectMapper objectMapper;
-
-    SlackApiClient() {
-        this.httpClient = 
HttpClient.newBuilder().connectTimeout(CONNECT_TIMEOUT).build();
-        this.objectMapper = new ObjectMapper();
-    }
-
-    AuthTestResponse authTest(String token) throws IOException, 
InterruptedException {
-        HttpRequest request = HttpRequest.newBuilder(AUTH_TEST_URI)
-                .timeout(REQUEST_TIMEOUT)
-                .header("Authorization", "Bearer " + token)
-                .GET()
-                .build();
-
-        HttpResponse<String> response = sendWithRetry(request);
-        if (response.statusCode() != 200) {
-            return AuthTestResponse.httpError(response.statusCode());
-        }
-
-        return objectMapper.readValue(response.body(), AuthTestResponse.class);
-    }
-
-    ConversationsListResponse listPublicChannels(String token)
-            throws IOException, InterruptedException {
-        List<SlackChannel> channels = new ArrayList<>();
-        String cursor = null;
-
-        do {
-            URI uri = buildConversationsListUri(cursor);
-            HttpRequest request = HttpRequest.newBuilder(uri)
-                    .timeout(REQUEST_TIMEOUT)
-                    .header("Authorization", "Bearer " + token)
-                    .GET()
-                    .build();
-
-            HttpResponse<String> response = sendWithRetry(request);
-            if (response.statusCode() != 200) {
-                return 
ConversationsListResponse.httpError(response.statusCode());
-            }
-
-            ConversationsListResponse payload = 
objectMapper.readValue(response.body(),
-                    ConversationsListResponse.class);
-            if (!payload.ok()) {
-                return new ConversationsListResponse(false, payload.error(), 
List.of(), null);
-            }
-
-            if (payload.channels() != null) {
-                channels.addAll(payload.channels());
-            }
-            cursor = payload.nextCursor();
-        } while (cursor != null && !cursor.isBlank());
-
-        return new ConversationsListResponse(true, null, 
List.copyOf(channels), null);
-    }
-
-    ConversationsHistoryResponse listChannelMessages(String token, String 
channelId,
-            String oldestTs) throws IOException, InterruptedException {
-        List<SlackMessage> messages = new ArrayList<>();
-        String cursor = null;
-
-        do {
-            URI uri = buildConversationsHistoryUri(channelId, oldestTs, 
cursor);
-            HttpRequest request = HttpRequest.newBuilder(uri)
-                    .timeout(REQUEST_TIMEOUT)
-                    .header("Authorization", "Bearer " + token)
-                    .GET()
-                    .build();
-
-            HttpResponse<String> response = sendWithRetry(request);
-            if (response.statusCode() != 200) {
-                return 
ConversationsHistoryResponse.httpError(response.statusCode());
-            }
-
-            ConversationsHistoryResponse payload = 
objectMapper.readValue(response.body(),
-                    ConversationsHistoryResponse.class);
-            if (!payload.ok()) {
-                return new ConversationsHistoryResponse(false, 
payload.error(), List.of(), null);
-            }
-
-            if (payload.messages() != null) {
-                messages.addAll(payload.messages());
-            }
-            cursor = payload.nextCursor();
-        } while (cursor != null && !cursor.isBlank());
-
-        return new ConversationsHistoryResponse(true, null, 
List.copyOf(messages), null);
-    }
-
-    ConversationsRepliesResponse listThreadReplies(String token, String 
channelId, String threadTs)
-            throws IOException, InterruptedException {
-        List<SlackMessage> messages = new ArrayList<>();
-        String cursor = null;
-
-        do {
-            URI uri = buildConversationsRepliesUri(channelId, threadTs, 
cursor);
-            HttpRequest request = HttpRequest.newBuilder(uri)
-                    .timeout(REQUEST_TIMEOUT)
-                    .header("Authorization", "Bearer " + token)
-                    .GET()
-                    .build();
-
-            HttpResponse<String> response = sendWithRetry(request);
-            if (response.statusCode() != 200) {
-                return 
ConversationsRepliesResponse.httpError(response.statusCode());
-            }
-
-            ConversationsRepliesResponse payload = 
objectMapper.readValue(response.body(),
-                    ConversationsRepliesResponse.class);
-            if (!payload.ok()) {
-                return new ConversationsRepliesResponse(false, 
payload.error(), List.of(), null);
-            }
-
-            if (payload.messages() != null) {
-                messages.addAll(payload.messages());
-            }
-            cursor = payload.nextCursor();
-        } while (cursor != null && !cursor.isBlank());
-
-        return new ConversationsRepliesResponse(true, null, 
List.copyOf(messages), null);
-    }
-
-    PermalinkResponse getPermalink(String token, String channelId, String 
messageTs)
-            throws IOException, InterruptedException {
-        URI uri = buildPermalinkUri(channelId, messageTs);
-        HttpRequest request = HttpRequest.newBuilder(uri)
-                .timeout(REQUEST_TIMEOUT)
-                .header("Authorization", "Bearer " + token)
-                .GET()
-                .build();
-
-        HttpResponse<String> response = sendWithRetry(request);
-        if (response.statusCode() != 200) {
-            return PermalinkResponse.httpError(response.statusCode());
-        }
-        return objectMapper.readValue(response.body(), 
PermalinkResponse.class);
-    }
-
-    UserInfoResponse getUserInfo(String token, String userId)
-            throws IOException, InterruptedException {
-        URI uri = buildUsersInfoUri(userId);
-        HttpRequest request = HttpRequest.newBuilder(uri)
-                .timeout(REQUEST_TIMEOUT)
-                .header("Authorization", "Bearer " + token)
-                .GET()
-                .build();
-
-        HttpResponse<String> response = sendWithRetry(request);
-        if (response.statusCode() != 200) {
-            return UserInfoResponse.httpError(response.statusCode());
-        }
-        return objectMapper.readValue(response.body(), UserInfoResponse.class);
-    }
-
-    private HttpResponse<String> sendWithRetry(HttpRequest request)
-            throws IOException, InterruptedException {
-        HttpResponse<String> response = httpClient.send(request,
-                HttpResponse.BodyHandlers.ofString());
-        if (response.statusCode() != 429) {
-            return response;
-        }
-        Optional<Duration> retryAfter = parseRetryAfter(response);
-        if (retryAfter.isEmpty()) {
-            return response;
-        }
-        Thread.sleep(retryAfter.get().toMillis());
-        return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-    }
-
-    private Optional<Duration> parseRetryAfter(HttpResponse<String> response) {
-        Optional<String> header = response.headers().firstValue("Retry-After");
-        if (header.isEmpty()) {
-            return Optional.empty();
-        }
-        try {
-            long seconds = Long.parseLong(header.get().trim());
-            return Optional.of(Duration.ofSeconds(seconds));
-        } catch (NumberFormatException ex) {
-            return Optional.empty();
-        }
-    }
-
-    private static URI buildConversationsListUri(String cursor) {
-        StringBuilder query = new StringBuilder();
-        query.append("limit=").append(CONVERSATIONS_PAGE_SIZE);
-        query.append("&exclude_archived=true");
-        query.append("&types=public_channel");
-        if (cursor != null && !cursor.isBlank()) {
-            query.append("&cursor=")
-                    .append(URLEncoder.encode(cursor, StandardCharsets.UTF_8));
-        }
-        return URI.create(CONVERSATIONS_LIST_URL + "?" + query);
-    }
-
-    private static URI buildConversationsHistoryUri(String channelId, String 
oldestTs,
-            String cursor) {
-        StringBuilder query = new StringBuilder();
-        query.append("channel=")
-                .append(URLEncoder.encode(channelId, StandardCharsets.UTF_8));
-        query.append("&limit=").append(HISTORY_PAGE_SIZE);
-        query.append("&inclusive=true");
-        if (oldestTs != null && !oldestTs.isBlank()) {
-            query.append("&oldest=")
-                    .append(URLEncoder.encode(oldestTs, 
StandardCharsets.UTF_8));
-        }
-        if (cursor != null && !cursor.isBlank()) {
-            query.append("&cursor=")
-                    .append(URLEncoder.encode(cursor, StandardCharsets.UTF_8));
-        }
-        return URI.create(CONVERSATIONS_HISTORY_URL + "?" + query);
-    }
-
-    private static URI buildPermalinkUri(String channelId, String messageTs) {
-        StringBuilder query = new StringBuilder();
-        query.append("channel=")
-                .append(URLEncoder.encode(channelId, StandardCharsets.UTF_8));
-        query.append("&message_ts=")
-                .append(URLEncoder.encode(messageTs, StandardCharsets.UTF_8));
-        return URI.create(CHAT_PERMALINK_URL + "?" + query);
-    }
-
-    private static URI buildUsersInfoUri(String userId) {
-        StringBuilder query = new StringBuilder();
-        query.append("user=").append(URLEncoder.encode(userId, 
StandardCharsets.UTF_8));
-        return URI.create(USERS_INFO_URL + "?" + query);
-    }
-
-    private static URI buildConversationsRepliesUri(String channelId, String 
threadTs,
-            String cursor) {
-        StringBuilder query = new StringBuilder();
-        query.append("channel=")
-                .append(URLEncoder.encode(channelId, StandardCharsets.UTF_8));
-        query.append("&ts=")
-                .append(URLEncoder.encode(threadTs, StandardCharsets.UTF_8));
-        query.append("&limit=").append(HISTORY_PAGE_SIZE);
-        if (cursor != null && !cursor.isBlank()) {
-            query.append("&cursor=")
-                    .append(URLEncoder.encode(cursor, StandardCharsets.UTF_8));
-        }
-        return URI.create(CONVERSATIONS_REPLIES_URL + "?" + query);
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record AuthTestResponse(boolean ok, String error, String team, String 
user) {
-        static AuthTestResponse httpError(int statusCode) {
-            return new AuthTestResponse(false, "http_status_" + statusCode, 
null, null);
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record ConversationsListResponse(boolean ok, String error, 
List<SlackChannel> channels,
-            @JsonProperty("response_metadata") ResponseMetadata 
responseMetadata) {
-        static ConversationsListResponse httpError(int statusCode) {
-            return new ConversationsListResponse(false, "http_status_" + 
statusCode, List.of(),
-                    null);
-        }
-
-        String nextCursor() {
-            if (responseMetadata == null) {
-                return null;
-            }
-            return responseMetadata.nextCursor();
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record ResponseMetadata(@JsonProperty("next_cursor") String nextCursor) {
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record SlackChannel(String id, String name) {
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record ConversationsHistoryResponse(boolean ok, String error, 
List<SlackMessage> messages,
-            @JsonProperty("response_metadata") ResponseMetadata 
responseMetadata) {
-        static ConversationsHistoryResponse httpError(int statusCode) {
-            return new ConversationsHistoryResponse(false, "http_status_" + 
statusCode, List.of(),
-                    null);
-        }
-
-        String nextCursor() {
-            if (responseMetadata == null) {
-                return null;
-            }
-            return responseMetadata.nextCursor();
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record ConversationsRepliesResponse(boolean ok, String error, 
List<SlackMessage> messages,
-            @JsonProperty("response_metadata") ResponseMetadata 
responseMetadata) {
-        static ConversationsRepliesResponse httpError(int statusCode) {
-            return new ConversationsRepliesResponse(false, "http_status_" + 
statusCode, List.of(),
-                    null);
-        }
-
-        String nextCursor() {
-            if (responseMetadata == null) {
-                return null;
-            }
-            return responseMetadata.nextCursor();
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record PermalinkResponse(boolean ok, String error, String permalink) {
-        static PermalinkResponse httpError(int statusCode) {
-            return new PermalinkResponse(false, "http_status_" + statusCode, 
null);
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record UserInfoResponse(boolean ok, String error, SlackUser user) {
-        static UserInfoResponse httpError(int statusCode) {
-            return new UserInfoResponse(false, "http_status_" + statusCode, 
null);
-        }
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record SlackUser(String id, String name, SlackProfile profile) {
-    }
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record SlackProfile(@JsonProperty("display_name") String displayName,
-            @JsonProperty("real_name") String realName) {
-    }
-}
-
diff --git a/src/main/java/org/apache/fineract/chat/archive/SlackMessage.java 
b/src/main/java/org/apache/fineract/chat/archive/SlackMessage.java
deleted file mode 100644
index 9909ff9..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/SlackMessage.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-record SlackMessage(String ts, String user, @JsonProperty("bot_id") String 
botId, String text,
-        String subtype, @JsonProperty("thread_ts") String threadTs, Edited 
edited) {
-
-    @JsonIgnoreProperties(ignoreUnknown = true)
-    record Edited(String ts) {
-    }
-}
diff --git a/src/main/java/org/apache/fineract/chat/archive/SlackTimestamp.java 
b/src/main/java/org/apache/fineract/chat/archive/SlackTimestamp.java
deleted file mode 100644
index 60fc2c9..0000000
--- a/src/main/java/org/apache/fineract/chat/archive/SlackTimestamp.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.Instant;
-import java.util.Locale;
-
-final class SlackTimestamp {
-
-    private SlackTimestamp() {}
-
-    static Instant toInstant(String ts) {
-        if (ts == null || ts.isBlank()) {
-            return Instant.EPOCH;
-        }
-        BigDecimal value = new BigDecimal(ts);
-        long seconds = value.longValue();
-        BigDecimal fractional = value.subtract(BigDecimal.valueOf(seconds));
-        int nanos = fractional.movePointRight(9).setScale(0, 
RoundingMode.DOWN).intValue();
-        return Instant.ofEpochSecond(seconds, nanos);
-    }
-
-    static int compare(String first, String second) {
-        if (first == null) {
-            return second == null ? 0 : -1;
-        }
-        if (second == null) {
-            return 1;
-        }
-        return new BigDecimal(first).compareTo(new BigDecimal(second));
-    }
-
-    static String formatEpochSecond(long epochSecond) {
-        return String.format(Locale.ROOT, "%d.000000", epochSecond);
-    }
-}
diff --git 
a/src/main/java/org/apache/fineract/chat/archive/UserDisplayNameResolver.java 
b/src/main/java/org/apache/fineract/chat/archive/UserDisplayNameResolver.java
deleted file mode 100644
index 5d72715..0000000
--- 
a/src/main/java/org/apache/fineract/chat/archive/UserDisplayNameResolver.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-final class UserDisplayNameResolver {
-
-    private UserDisplayNameResolver() {}
-
-    static String resolve(SlackApiClient.SlackUser user) {
-        if (user == null) {
-            return "unknown";
-        }
-        SlackApiClient.SlackProfile profile = user.profile();
-        if (profile != null) {
-            String displayName = clean(profile.displayName());
-            if (!displayName.isEmpty()) {
-                return displayName;
-            }
-            String realName = clean(profile.realName());
-            if (!realName.isEmpty()) {
-                return realName;
-            }
-        }
-        String name = clean(user.name());
-        if (!name.isEmpty()) {
-            return name;
-        }
-        String id = clean(user.id());
-        return id.isEmpty() ? "unknown" : id;
-    }
-
-    private static String clean(String value) {
-        return value == null ? "" : value.trim();
-    }
-}
diff --git 
a/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java 
b/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java
deleted file mode 100644
index 8b868df..0000000
--- a/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Optional;
-import org.junit.jupiter.api.Test;
-
-class ArchiveConfigTest {
-
-    @Test
-    void loadReturnsConfigWhenAllowlistPresent() throws Exception {
-        Path tempFile = Files.createTempFile("archive", ".properties");
-        String content = String.join("\n",
-                "channels.allowlist=#fineract, dev",
-                "output.dir=docs",
-                "state.dir=state",
-                "fetch.lookback.days=3",
-                "");
-        Files.writeString(tempFile, content, StandardCharsets.UTF_8);
-
-        Optional<ArchiveConfig> config = ArchiveConfig.load(tempFile);
-
-        assertTrue(config.isPresent());
-        assertEquals(2, config.get().channelAllowlist().size());
-        assertEquals("fineract", config.get().channelAllowlist().get(0));
-        assertEquals("dev", config.get().channelAllowlist().get(1));
-        assertEquals(Path.of("docs"), config.get().outputDir());
-        assertEquals(Path.of("state"), config.get().stateDir());
-        assertEquals(3, config.get().lookbackDays());
-    }
-
-    @Test
-    void loadReturnsEmptyWhenFileMissing() throws Exception {
-        Path missingFile = Path.of("config",
-                "missing-" + System.nanoTime() + ".properties");
-
-        Optional<ArchiveConfig> config = ArchiveConfig.load(missingFile);
-
-        assertTrue(config.isEmpty());
-    }
-
-    @Test
-    void loadReturnsEmptyWhenAllowlistIsEmpty() throws Exception {
-        Path tempFile = Files.createTempFile("archive-empty", ".properties");
-        String content = String.join("\n",
-                "channels.allowlist=",
-                "output.dir=docs",
-                "");
-        Files.writeString(tempFile, content, StandardCharsets.UTF_8);
-
-        Optional<ArchiveConfig> config = ArchiveConfig.load(tempFile);
-
-        assertTrue(config.isEmpty());
-    }
-
-    @Test
-    void loadUsesDefaultLookbackDaysWhenInvalid() throws Exception {
-        Path tempFile = Files.createTempFile("archive-invalid", ".properties");
-        String content = String.join("\n",
-                "channels.allowlist=#fineract",
-                "fetch.lookback.days=0",
-                "");
-        Files.writeString(tempFile, content, StandardCharsets.UTF_8);
-
-        Optional<ArchiveConfig> config = ArchiveConfig.load(tempFile);
-
-        assertTrue(config.isPresent());
-        assertEquals(ArchiveConfig.DEFAULT_LOOKBACK_DAYS, 
config.get().lookbackDays());
-    }
-}
-
diff --git 
a/src/test/java/org/apache/fineract/chat/archive/ChannelResolverTest.java 
b/src/test/java/org/apache/fineract/chat/archive/ChannelResolverTest.java
deleted file mode 100644
index a9ca63d..0000000
--- a/src/test/java/org/apache/fineract/chat/archive/ChannelResolverTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.List;
-import org.junit.jupiter.api.Test;
-
-class ChannelResolverTest {
-
-    @Test
-    void resolveMatchesAllowlistIgnoringCase() {
-        List<String> allowlist = List.of("fineract", "Dev");
-        List<SlackApiClient.SlackChannel> channels = List.of(
-                new SlackApiClient.SlackChannel("C1", "fineract"),
-                new SlackApiClient.SlackChannel("C2", "dev"));
-
-        ChannelResolver.ChannelResolution resolution = 
ChannelResolver.resolve(allowlist,
-                channels);
-
-        assertEquals(2, resolution.resolved().size());
-        assertTrue(resolution.missing().isEmpty());
-    }
-
-    @Test
-    void resolveReportsMissingChannels() {
-        List<String> allowlist = List.of("fineract", "unknown");
-        List<SlackApiClient.SlackChannel> channels = List.of(
-                new SlackApiClient.SlackChannel("C1", "fineract"));
-
-        ChannelResolver.ChannelResolution resolution = 
ChannelResolver.resolve(allowlist,
-                channels);
-
-        assertEquals(1, resolution.resolved().size());
-        assertEquals(List.of("unknown"), resolution.missing());
-    }
-}
diff --git 
a/src/test/java/org/apache/fineract/chat/archive/UserDisplayNameResolverTest.java
 
b/src/test/java/org/apache/fineract/chat/archive/UserDisplayNameResolverTest.java
deleted file mode 100644
index 7eac984..0000000
--- 
a/src/test/java/org/apache/fineract/chat/archive/UserDisplayNameResolverTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.chat.archive;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-class UserDisplayNameResolverTest {
-
-    @Test
-    void resolvePrefersDisplayName() {
-        SlackApiClient.SlackProfile profile = new 
SlackApiClient.SlackProfile("Ada", "Ada L");
-        SlackApiClient.SlackUser user = new SlackApiClient.SlackUser("U1", 
"ada", profile);
-
-        assertEquals("Ada", UserDisplayNameResolver.resolve(user));
-    }
-
-    @Test
-    void resolveFallsBackToRealName() {
-        SlackApiClient.SlackProfile profile = new 
SlackApiClient.SlackProfile(" ", "Ada Lovelace");
-        SlackApiClient.SlackUser user = new SlackApiClient.SlackUser("U2", 
"ada", profile);
-
-        assertEquals("Ada Lovelace", UserDisplayNameResolver.resolve(user));
-    }
-
-    @Test
-    void resolveFallsBackToUserName() {
-        SlackApiClient.SlackProfile profile = new 
SlackApiClient.SlackProfile(null, null);
-        SlackApiClient.SlackUser user = new SlackApiClient.SlackUser("U3", 
"ada", profile);
-
-        assertEquals("ada", UserDisplayNameResolver.resolve(user));
-    }
-}


Reply via email to