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));
- }
-}