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

commit 55b4df23737e78f3cc6b948282f933e718f80f86
Author: Adam Monsen <[email protected]>
AuthorDate: Mon Feb 9 16:34:19 2026 -0800

    use env vars instead of properties file for config
    
    * move slack token into ArchiveConfig class
    * log all config on startup
    * remove Optional; guarantee callers have useful config instance
    
    Partially AI-assisted by Kimi K2.5 Free OpenCode Zen, but I had to clean up 
quite a bit manually.
---
 Readme.md                                          | 22 ++++---
 config/archive.properties                          | 23 -------
 .../fineract/chat/archive/ArchiveConfig.java       | 55 ++++++++--------
 .../fineract/chat/archive/ChatArchiveApp.java      | 63 ++++++-------------
 .../fineract/chat/archive/ArchiveConfigTest.java   | 73 ++++++++--------------
 5 files changed, 83 insertions(+), 153 deletions(-)

diff --git a/Readme.md b/Readme.md
index 72c0396..32312d8 100644
--- a/Readme.md
+++ b/Readme.md
@@ -4,27 +4,27 @@ A standalone tool for archiving Slack messages into a static 
site.
 
 ## Local run
 
-```
+```bash
 ./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`)
+Configuration (environment variables):
+
+- `SLACK_TOKEN` (required; Slack Bot token) ⚠️ warning: this keep secret!
+- `CHANNELS_ALLOWLIST` (required; comma-separated channel names, e.g. 
`#fineract`)
+- `OUTPUT_DIR` (optional; relative path for site output, default `docs`)
+- `STATE_DIR` (optional; relative path for cursor state, default `state`)
+- `LOOKBACK_DAYS` (optional; 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.
@@ -32,9 +32,11 @@ Slack app setup:
 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.
 
diff --git a/config/archive.properties b/config/archive.properties
deleted file mode 100644
index 76bfb28..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=2
-
diff --git a/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java 
b/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java
index cbd62d5..37b6a42 100644
--- a/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java
+++ b/src/main/java/org/apache/fineract/chat/archive/ArchiveConfig.java
@@ -18,58 +18,55 @@
  */
 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 SLACK_TOKEN_ENV = "SLACK_TOKEN";
 
-    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 String CHANNELS_ALLOWLIST_ENV = "CHANNELS_ALLOWLIST";
+    static final String OUTPUT_DIR_ENV = "OUTPUT_DIR";
+    static final String STATE_DIR_ENV = "STATE_DIR";
+    static final String LOOKBACK_DAYS_ENV = "LOOKBACK_DAYS";
+    static final String LOG_LEVEL_ENV = "LOG_LEVEL";
+
+    static final String DEFAULT_OUTPUT_DIR = "docs";
+    static final String DEFAULT_STATE_DIR = "state";
     static final int DEFAULT_LOOKBACK_DAYS = 1;
 
+    private final String slackToken;
     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,
+    private ArchiveConfig(String slackToken, List<String> channelAllowlist, 
Path outputDir, Path stateDir,
             int lookbackDays) {
+        this.slackToken = slackToken;
         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);
-        }
+    static ArchiveConfig fromEnv() {
+        return fromValues(System.getenv(SLACK_TOKEN_ENV), 
System.getenv(CHANNELS_ALLOWLIST_ENV), System.getenv(OUTPUT_DIR_ENV),
+                System.getenv(STATE_DIR_ENV), 
System.getenv(LOOKBACK_DAYS_ENV));
+    }
 
-        String allowlist = properties.getProperty(CHANNELS_ALLOWLIST_KEY, "");
+    static ArchiveConfig fromValues(String slackTokenValue, String allowlist, 
String outputDirValue,
+            String stateDirValue, String lookbackDaysValue) {
+        String slackToken = slackTokenValue != null ? slackTokenValue.trim() : 
"";
         List<String> channels = parseAllowlist(allowlist);
-        if (channels.isEmpty()) {
-            return Optional.empty();
-        }
+        Path outputDir = Path.of(outputDirValue != null ? 
outputDirValue.trim() : DEFAULT_OUTPUT_DIR);
+        Path stateDir = Path.of(stateDirValue != null ? stateDirValue.trim() : 
DEFAULT_STATE_DIR);
+        int lookbackDays = parseLookbackDays(lookbackDaysValue);
+        return new ArchiveConfig(slackToken, channels, outputDir, stateDir, 
lookbackDays);
+    }
 
-        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));
+    String slackToken() {
+        return slackToken;
     }
 
     List<String> channelAllowlist() {
diff --git a/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java 
b/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java
index 30e64b6..d66a797 100644
--- a/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java
+++ b/src/main/java/org/apache/fineract/chat/archive/ChatArchiveApp.java
@@ -25,56 +25,39 @@ 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.*;
 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;
-        }
+        ArchiveConfig config = ArchiveConfig.fromEnv();
 
-        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);
+        String slackToken = config.slackToken();
+        if (slackToken.isEmpty()) {
+            LOG.info(ArchiveConfig.SLACK_TOKEN_ENV + " is not set. Skipping 
archive update.");
             return;
         }
 
-        if (config.isEmpty()) {
-            LOG.info("Config file missing or channel allowlist empty. Skipping 
archive update.");
+        if (config.channelAllowlist().isEmpty()) {
+            LOG.info(ArchiveConfig.CHANNELS_ALLOWLIST_ENV + " is not set. 
Skipping archive update.");
             return;
         }
 
-        ArchiveConfig archiveConfig = config.get();
-        LOG.info("Loaded config for " + archiveConfig.channelAllowlist().size()
-                + " channel(s).");
+        LOG.info("Using state dir [" + config.stateDir() + "]");
+        LOG.info("Using output dir [" + config.outputDir() + "]");
+        LOG.info("Loaded config for " + config.channelAllowlist().size() + " 
channel(s).");
+        LOG.info("Will fetch messages for the past " + config.lookbackDays() + 
" day(s).");
 
         SlackApiClient slackApiClient = new SlackApiClient();
         SlackApiClient.AuthTestResponse authResponse;
         try {
-            authResponse = slackApiClient.authTest(slackToken.get());
+            authResponse = slackApiClient.authTest(config.slackToken());
         } catch (IOException ex) {
             LOG.log(Level.SEVERE, "Slack auth.test call failed.", ex);
             return;
@@ -93,7 +76,7 @@ public final class ChatArchiveApp {
 
         SlackApiClient.ConversationsListResponse channelsResponse;
         try {
-            channelsResponse = 
slackApiClient.listPublicChannels(slackToken.get());
+            channelsResponse = 
slackApiClient.listPublicChannels(config.slackToken());
         } catch (IOException ex) {
             LOG.log(Level.SEVERE, "Slack conversations.list call failed.", ex);
             return;
@@ -110,7 +93,7 @@ public final class ChatArchiveApp {
 
         List<SlackApiClient.SlackChannel> channels = 
channelsResponse.channels();
         ChannelResolver.ChannelResolution resolution = ChannelResolver.resolve(
-                archiveConfig.channelAllowlist(), channels);
+                config.channelAllowlist(), channels);
 
         if (!resolution.missing().isEmpty()) {
             LOG.warning("Allowlisted channel(s) not found: "
@@ -125,14 +108,14 @@ public final class ChatArchiveApp {
         LOG.info("Resolved " + resolution.resolved().size() + " channel(s).");
 
         Instant windowStart = Instant.now()
-                .minus(Duration.ofDays(archiveConfig.lookbackDays()));
+                .minus(Duration.ofDays(config.lookbackDays()));
         String windowOldest = 
SlackTimestamp.formatEpochSecond(windowStart.getEpochSecond());
 
-        CursorStore cursorStore = new CursorStore(archiveConfig.stateDir());
+        CursorStore cursorStore = new CursorStore(config.stateDir());
         CursorStore.CursorState cursorState = loadCursorState(cursorStore);
         Map<String, String> cursors = new HashMap<>(cursorState.channels());
 
-        Path dailyRoot = archiveConfig.outputDir().resolve("daily");
+        Path dailyRoot = config.outputDir().resolve("daily");
         Map<String, String> permalinkCache = new HashMap<>();
         Map<String, String> userCache = new HashMap<>();
         Map<String, List<SlackMessage>> threadRepliesCache = new HashMap<>();
@@ -144,7 +127,7 @@ public final class ChatArchiveApp {
                     cursors.get(channelId));
             SlackApiClient.ConversationsHistoryResponse historyResponse;
             try {
-                historyResponse = 
slackApiClient.listChannelMessages(slackToken.get(), channelId,
+                historyResponse = 
slackApiClient.listChannelMessages(config.slackToken(), channelId,
                         oldest);
             } catch (IOException ex) {
                 LOG.log(Level.SEVERE, "Slack conversations.history call failed 
for channel "
@@ -174,7 +157,7 @@ public final class ChatArchiveApp {
             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,
+                        config.slackToken(), slackApiClient, permalinkCache, 
userCache,
                         threadRepliesCache);
                 String page = MarkdownRenderer.renderDailyPage(channel.name(), 
date, rows);
                 Path pagePath = dailyRoot.resolve(channel.name()).resolve(date 
+ ".md");
@@ -201,14 +184,6 @@ public final class ChatArchiveApp {
         }
     }
 
-    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);
diff --git 
a/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java 
b/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java
index 8b868df..1521052 100644
--- a/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java
+++ b/src/test/java/org/apache/fineract/chat/archive/ArchiveConfigTest.java
@@ -21,73 +21,52 @@ 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);
+    void nominalConfig() {
 
-        Optional<ArchiveConfig> config = ArchiveConfig.load(tempFile);
+        ArchiveConfig config = ArchiveConfig.fromValues("dummy", "#fineract, 
dev", "docs",
+                "state", "3");
 
-        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());
+        assertEquals(2, config.channelAllowlist().size());
+        assertEquals("fineract", config.channelAllowlist().get(0));
+        assertEquals("dev", config.channelAllowlist().get(1));
+        assertEquals(Path.of("docs"), config.outputDir());
+        assertEquals(Path.of("state"), config.stateDir());
+        assertEquals(3, config.lookbackDays());
     }
 
     @Test
-    void loadReturnsEmptyWhenFileMissing() throws Exception {
-        Path missingFile = Path.of("config",
-                "missing-" + System.nanoTime() + ".properties");
+    void missingSlackToken() {
+        ArchiveConfig config = ArchiveConfig.fromValues(" ",null, "docs", 
"state", "1");
 
-        Optional<ArchiveConfig> config = ArchiveConfig.load(missingFile);
-
-        assertTrue(config.isEmpty());
+        assertTrue(config.slackToken().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);
+    void emptyAllowList() {
+        ArchiveConfig config = ArchiveConfig.fromValues("dummy","", "docs", 
"state", "1");
 
-        assertTrue(config.isEmpty());
+        assertTrue(config.channelAllowlist().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);
+    void fromValuesUsesDefaultLookbackDaysWhenInvalid() {
+        ArchiveConfig config = ArchiveConfig.fromValues("dummy","#fineract", 
"docs", "state", "0");
+
+        assertEquals(ArchiveConfig.DEFAULT_LOOKBACK_DAYS, 
config.lookbackDays());
+    }
 
-        Optional<ArchiveConfig> config = ArchiveConfig.load(tempFile);
+    @Test
+    void fromValuesUsesDefaultsWhenOptionalValuesNull() {
+        ArchiveConfig config = ArchiveConfig.fromValues("dummy", "#fineract", 
null, null, null);
 
-        assertTrue(config.isPresent());
-        assertEquals(ArchiveConfig.DEFAULT_LOOKBACK_DAYS, 
config.get().lookbackDays());
+        assertEquals(Path.of(ArchiveConfig.DEFAULT_OUTPUT_DIR), 
config.outputDir());
+        assertEquals(Path.of(ArchiveConfig.DEFAULT_STATE_DIR), 
config.stateDir());
+        assertEquals(ArchiveConfig.DEFAULT_LOOKBACK_DAYS, 
config.lookbackDays());
     }
 }
-

Reply via email to