Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/release-notes/v1.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Firefly v1.0.0

First stable release of **Firefly**: a Spring Boot Telegram bot for DTEK outage schedules (Kyiv + Kyiv region), with SQLite persistence and automated schedule refresh.

### Highlights
- Telegram bot with `/start` flow and inline menu.
- Manual outage group selection for:
- Kyiv (`🏙️ Київ`)
- Kyiv region (`🏘️ Київщина`)
- Auto-detect group by Kyiv address via YASNO (`📍 Знайти групу за адресою (Київ)`).
- Per-user notification toggle and persisted user settings.
- Admin stats commands:
- `/stats_today`
- `/stats_week`
- Scheduled schedule refresh + midnight rollover logic (`Europe/Kyiv`).

### Tech Stack
- Java 25
- Spring Boot 4.0.1
- SQLite + Spring Data JPA + Flyway
- Telegram Bots API (long polling)
- Playwright + Jsoup

### Breaking Changes
- None (initial stable release).

### Install / Run (JAR)
Prerequisite: Java 25

```bash
export TELEGRAM_BOT_TOKEN=your_token_here
java -jar firefly-1.0.0.jar
```

Optional env vars:
- `TELEGRAM_ADMIN_CHAT_IDS`
- `SPRING_DATASOURCE_URL` (default: `jdbc:sqlite:src/main/resources/db/app.db`)
- `scheduler.shutdowns.fixed-delay-ms` (interpreted as minutes)
- `scheduler.shutdowns.time-zone` (default: `Europe/Kyiv`)

### Database Notes
- Flyway migrations are enabled.
- `spring.jpa.hibernate.ddl-auto=none`.

### Checksums
`SHA-256`:

- `firefly-1.0.0.jar`: `99fdb3b2ef1e34f7231e696416df7f801de9ed2f5a91b652de9c2b0b30c95e0d`

Verification:
```bash
shasum -a 256 firefly-1.0.0.jar
```

### Docker
Also available as container image:
- `ghcr.io/ndmik-dev/firefly:latest`

---

**Full Changelog**: Initial stable release (`v1.0.0`).
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Changelog

All notable changes to this project are documented in this file.

## [1.0.0] - 2026-03-13

### Added
- Initial stable Firefly release.
- Telegram bot `/start` flow with inline menu and persisted user settings.
- Manual outage group selection for Kyiv and Kyiv region.
- YASNO-based address-to-group resolution for Kyiv addresses.
- Per-user notification toggle.
- Admin stats commands: `/stats_today`, `/stats_week`.
- Scheduler refresh for outage schedules and midnight rollover handling.

### Technical
- Spring Boot 4.0.1, Java 25.
- SQLite persistence with Spring Data JPA and Flyway migrations.
- DTEK/YASNO integrations using Playwright + Jsoup.

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /ms-playwright /ms-playwright
COPY --from=builder /app/build/libs/firefly-0.0.1.jar /app/app.jar
COPY --from=builder /app/build/libs/firefly-1.0.0.jar /app/app.jar

ENTRYPOINT ["java","-jar","/app/app.jar"]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ App listens on `8080`.
./gradlew build
```

## Run Released JAR (`v1.0.0`)
```bash
export TELEGRAM_BOT_TOKEN=your_token_here
java -jar build/libs/firefly-1.0.0.jar
```

## YASNO Address Lookup Script
Script: `scripts/yasno_group_by_address.sh`

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'io.ndmik'
version = '0.0.1'
version = '1.0.0'
description = 'firefly'

java {
Expand Down
47 changes: 44 additions & 3 deletions src/main/java/ua/ndmik/bot/client/YasnoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class YasnoClient {
private static final String STREETS_PATH = "/streets";
private static final String HOUSES_PATH = "/houses";
private static final String GROUP_PATH = "/group";
public static final String DEFAULT_SUBGROUP = ".1";

private final RestClient restClient;
private final JsonMapper mapper;
Expand Down Expand Up @@ -137,11 +138,21 @@ private String extractGroupId(String json) {
log.warn("Unexpected non-JSON YASNO group payload: {}", snippet(json));
return "";
}
String compositeFromRoot = groupWithSubgroup(root);
if (!compositeFromRoot.isBlank()) {
return compositeFromRoot;
}

String compositeFromData = groupWithSubgroup(root.path("data"));
if (!compositeFromData.isBlank()) {
return compositeFromData;
}

JsonNode candidate = firstPresent(
root.path("group"),
root.path("groupId"),
root.path("data").path("group"),
root.path("data").path("groupId")
root.path("group"),
root.path("data").path("groupId"),
root.path("data").path("group")
);

if (candidate == null || candidate.isMissingNode() || candidate.isNull()) {
Expand Down Expand Up @@ -179,6 +190,36 @@ private JsonNode firstPresent(JsonNode... candidates) {
return null;
}

private String groupWithSubgroup(JsonNode node) {
if (node == null || node.isMissingNode() || node.isNull()) {
return "";
}

JsonNode groupNode = firstPresent(node.path("groupId"), node.path("group"));
if (groupNode == null || groupNode.isMissingNode() || groupNode.isNull()) {
return "";
}
String group = text(groupNode).trim();
if (group.isBlank()) {
return "";
}

JsonNode subgroupNode = firstPresent(node.path("subgroup"), node.path("subGroup"));
if (subgroupNode == null || subgroupNode.isMissingNode() || subgroupNode.isNull()) {
return group.contains(".") ? group : group + DEFAULT_SUBGROUP;
}

String subgroup = text(subgroupNode).trim();
if (group.contains(".")) {
return group;
}
if (subgroup.isBlank()) {
return group + DEFAULT_SUBGROUP;
}

return group + "." + subgroup;
}

private JsonNode firstArray(JsonNode... candidates) {
for (JsonNode candidate : candidates) {
if (candidate != null && candidate.isArray()) {
Expand Down
40 changes: 38 additions & 2 deletions src/test/java/ua/ndmik/bot/client/YasnoClientTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,48 @@ void findStreets_ignoresNonJsonPayload() {
}

@Test
void findGroup_extractsNumericGroup() {
void findGroup_combinesGroupWithSubgroup() {
YasnoClient client = clientWithResponse("{\"group\":20,\"subgroup\":1}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("20");
assertThat(group).isEqualTo("20.1");
}

@Test
void findGroup_keepsGroupIdThatAlreadyContainsSubgroup() {
YasnoClient client = clientWithResponse("{\"groupId\":\"29.1\",\"subgroup\":1}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("29.1");
}

@Test
void findGroup_combinesGroupWithSubgroupFromDataNode() {
YasnoClient client = clientWithResponse("{\"data\":{\"group\":20,\"subgroup\":1}}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("20.1");
}

@Test
void findGroup_defaultsMissingSubgroupToOne() {
YasnoClient client = clientWithResponse("{\"group\":20}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("20.1");
}

@Test
void findGroup_defaultsMissingSubgroupToOneInDataNode() {
YasnoClient client = clientWithResponse("{\"data\":{\"group\":29}}");

String group = client.findGroup(25, 902, 1064, 17211);

assertThat(group).isEqualTo("29.1");
}

@SuppressWarnings("unchecked")
Expand Down
Loading