Skip to content

Commit 9e9535b

Browse files
committed
v0.1
1 parent cf18969 commit 9e9535b

15 files changed

+234
-205
lines changed

.idea/codeStyles/Project.xml

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle.kts

+4-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010
}
1111

1212
group = "com.github.smaugfm"
13-
version = "1.0.0"
13+
version = "0.1-alpha"
1414

1515
val myMavenRepoReadUrl: String by project
1616
val myMavenRepoReadUsername: String by project
@@ -60,21 +60,14 @@ tasks {
6060
shadowJar {
6161
archiveBaseName.set(rootProject.name)
6262
archiveClassifier.set("fat")
63+
manifest {
64+
attributes(mapOf("Main-Class" to "com.github.smaugfm.YnabMonoKt"))
65+
}
6366
}
6467

6568
build {
6669
dependsOn(shadowJar)
6770
}
68-
69-
register<JavaExec>("runFat") {
70-
group = "execution"
71-
main = "com.github.smaugfm.YnabMonoKt"
72-
classpath = files(
73-
rootDir.resolve(
74-
"build/libs/${rootProject.name}-${version}-fat.jar"
75-
)
76-
)
77-
}
7871
}
7972

8073
dependencies {

src/main/kotlin/com/github/smaugfm/YnabMono.kt

+8-7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package com.github.smaugfm
22

33
import com.github.ajalt.clikt.core.CliktCommand
44
import com.github.ajalt.clikt.parameters.options.convert
5+
import com.github.ajalt.clikt.parameters.options.default
56
import com.github.ajalt.clikt.parameters.options.flag
67
import com.github.ajalt.clikt.parameters.options.option
78
import com.github.ajalt.clikt.parameters.options.required
9+
import com.github.ajalt.clikt.parameters.types.int
810
import com.github.smaugfm.events.EventDispatcher
911
import com.github.smaugfm.mono.MonoApi
1012
import com.github.smaugfm.mono.MonoApi.Companion.setupWebhook
@@ -23,20 +25,19 @@ import java.util.concurrent.Executors
2325
private val logger = KotlinLogging.logger {}
2426

2527
class YnabMono : CliktCommand() {
26-
val dontSetWebhook by option().flag(default = true)
28+
val dontSetWebhook by option().flag(default = false)
2729
val monoWebhookUrl by option().convert { URI(it) }.required()
28-
val telegramWebhookUrl
29-
by option().convert { URI(it).also { uri -> assert(uri.toString().startsWith("https")) } }
30-
val settings by option("--settings").convert { Settings.load(Paths.get(it)) }.required()
30+
val monoWebhookPort by option().int()
31+
val settings by option("--settings").convert { Settings.load(Paths.get(it)) }.default(Settings.loadDefault())
3132

3233
private val serversCoroutinesContext = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
3334

3435
override fun run() {
3536
logger.info(
3637
"Input arguments:\n\t" +
3738
"${this::settings.name}: $settings\n\t" +
38-
"${this::telegramWebhookUrl.name}: $telegramWebhookUrl\n\t" +
3939
"${this::monoWebhookUrl.name}: $monoWebhookUrl\n\t" +
40+
"${this::monoWebhookPort.name}: $monoWebhookPort\n\t" +
4041
"${this::dontSetWebhook.name}: $dontSetWebhook",
4142
)
4243

@@ -48,14 +49,13 @@ class YnabMono : CliktCommand() {
4849
settings.telegramBotUsername,
4950
settings.telegramBotToken,
5051
settings.mappings.getTelegramChatIds(),
51-
telegramWebhookUrl
5252
)
5353
logger.info("Created telegram api.")
5454
val ynabApi = YnabApi(settings.ynabToken, settings.ynabBudgetId)
5555
logger.info("Created ynab api.")
5656

5757
if (!dontSetWebhook) {
58-
monoApis.setupWebhook(monoWebhookUrl)
58+
monoApis.setupWebhook(monoWebhookUrl, monoWebhookPort ?: monoWebhookUrl.port)
5959
logger.info("Mono webhook setup successful. $monoWebhookUrl")
6060
} else {
6161
logger.info("Skipping mono webhook setup.")
@@ -76,6 +76,7 @@ class YnabMono : CliktCommand() {
7676
MonoApi.startMonoWebhookServerAsync(
7777
serversCoroutinesContext,
7878
monoWebhookUrl,
79+
monoWebhookPort ?: monoWebhookUrl.port,
7980
dispatcher
8081
)
8182
logger.info("Mono webhook listener started.")

src/main/kotlin/com/github/smaugfm/events/Event.kt

+10-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.github.smaugfm.events
22

33
import com.github.smaugfm.mono.MonoWebHookResponseData
44
import com.github.smaugfm.telegram.TransactionActionType
5-
import com.github.smaugfm.ynab.YnabPayee
65
import com.github.smaugfm.ynab.YnabTransactionDetail
76

87
sealed class Event {
@@ -11,8 +10,11 @@ sealed class Event {
1110
}
1211

1312
sealed class Ynab : Event() {
14-
data class TransactionAction(val type: TransactionActionType) : Ynab(), UnitEvent
15-
object GetPayees : Ynab(), IEvent<List<YnabPayee>>
13+
data class TransactionAction(
14+
val payee: String?,
15+
val transactionId: String,
16+
val type: TransactionActionType
17+
) : Ynab(), UnitEvent
1618
}
1719

1820
sealed class Telegram : Event() {
@@ -21,9 +23,11 @@ sealed class Event {
2123
val transaction: YnabTransactionDetail,
2224
) : Telegram(), UnitEvent
2325

24-
data class CallbackQueryReceived(val callbackQueryId: String, val data: String) :
26+
data class CallbackQueryReceived(
27+
val callbackQueryId: String,
28+
val data: String,
29+
val messageText: String,
30+
) :
2531
Telegram(), UnitEvent
26-
27-
data class AnswerCallbackQuery(val callbackQueryId: String, val text: String?) : Telegram(), UnitEvent
2832
}
2933
}

src/main/kotlin/com/github/smaugfm/events/EventDispatcher.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ open class EventDispatcher(
1616

1717
override suspend fun <R, E : IEvent<R>> invoke(event: E): R {
1818
@Suppress("UNCHECKED_CAST")
19+
logger.info("Event dispatched: $event")
1920
val handler = handlers[event.javaClass] as? IEventHandler<R, E>
2021
?: throw IllegalStateException("No handler found for event $event, ${event.javaClass}")
2122

22-
return handler.handle(this, event)
23+
return handler.handle(this, event).also {
24+
logger.info("Event handled: $it")
25+
}
2326
}
2427
}

src/main/kotlin/com/github/smaugfm/mono/MonoApi.kt

+22-12
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import com.github.smaugfm.events.IEventDispatcher
55
import io.ktor.application.call
66
import io.ktor.application.install
77
import io.ktor.client.HttpClient
8+
import io.ktor.client.features.ResponseException
89
import io.ktor.client.features.defaultRequest
910
import io.ktor.client.features.json.JsonFeature
1011
import io.ktor.client.features.json.defaultSerializer
1112
import io.ktor.client.features.json.serializer.KotlinxSerializer
1213
import io.ktor.client.request.get
1314
import io.ktor.client.request.header
1415
import io.ktor.client.request.post
16+
import io.ktor.client.statement.readText
1517
import io.ktor.features.ContentNegotiation
1618
import io.ktor.http.ContentType
1719
import io.ktor.http.HttpStatusCode
@@ -24,6 +26,7 @@ import io.ktor.routing.routing
2426
import io.ktor.serialization.json
2527
import io.ktor.server.engine.embeddedServer
2628
import io.ktor.server.netty.Netty
29+
import kotlinx.coroutines.CompletableDeferred
2730
import kotlinx.coroutines.GlobalScope
2831
import kotlinx.coroutines.Job
2932
import kotlinx.coroutines.delay
@@ -59,24 +62,33 @@ class MonoApi(private val token: String) {
5962
return Json.decodeFromString(infoString)
6063
}
6164

62-
suspend fun setWebHook(url: URI): MonoStatusResponse {
65+
suspend fun setWebHook(url: URI, port: Int): MonoStatusResponse {
6366
require(url.toASCIIString() == url.toString())
6467

68+
val waitForWebhook = CompletableDeferred<Unit>()
6569
val json = defaultSerializer()
66-
val server = embeddedServer(Netty, port = url.port) {
70+
val server = embeddedServer(Netty, port = port) {
6771
routing {
6872
get(url.path) {
6973
call.response.status(HttpStatusCode.OK)
7074
call.respondText("OK\n", ContentType.Text.Plain)
7175
logger.info("Webhook setup successful: $url")
76+
waitForWebhook.complete(Unit)
7277
}
7378
}
7479
}
75-
logger.info("Starting webhook setup server.")
80+
logger.info("Starting webhook setup server...")
7681
server.start(wait = false)
77-
val statusString = httpClient.post<String>(url("personal/webhook")) {
78-
body = json.write(MonoWebHookRequest(url.toString()))
82+
83+
val statusString = try {
84+
httpClient.post<String>(url("personal/webhook")) {
85+
body = json.write(MonoWebHookRequest(url.toString()))
86+
}
87+
} catch (e: ResponseException) {
88+
logger.error(e.response.readText())
89+
throw e
7990
}
91+
waitForWebhook.await()
8092
server.stop(serverStopGracePeriod, serverStopGracePeriod)
8193
return Json.decodeFromString(statusString)
8294
}
@@ -112,9 +124,10 @@ class MonoApi(private val token: String) {
112124
fun startMonoWebhookServerAsync(
113125
context: CoroutineContext,
114126
webhook: URI,
115-
dispatcher: IEventDispatcher
127+
port: Int,
128+
dispatcher: IEventDispatcher,
116129
): Job {
117-
val server = embeddedServer(Netty, port = webhook.port) {
130+
val server = embeddedServer(Netty, port = port) {
118131
install(ContentNegotiation) {
119132
json()
120133
}
@@ -132,10 +145,7 @@ class MonoApi(private val token: String) {
132145
}
133146
}
134147

135-
suspend fun Collection<MonoApi>.setupWebhook(webhook: URI) {
136-
this.forEach {
137-
it.setWebHook(webhook)
138-
}
139-
}
148+
suspend fun Collection<MonoApi>.setupWebhook(webhook: URI, port: Int) =
149+
this.forEach { it.setWebHook(webhook, port) }
140150
}
141151
}

src/main/kotlin/com/github/smaugfm/mono/MonoStatementItem.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.smaugfm.mono
33
import com.github.smaugfm.serializers.CurrencyAsIntSerializer
44
import com.github.smaugfm.serializers.InstantAsLongSerializer
55
import com.github.smaugfm.util.MCC
6+
import com.github.smaugfm.util.replaceNewLines
67
import kotlinx.datetime.Instant
78
import kotlinx.serialization.Serializable
89
import java.util.Currency
@@ -28,12 +29,11 @@ data class MonoStatementItem(
2829
val balance: Long,
2930
val hold: Boolean,
3031
) {
31-
3232
override fun toString(): String {
3333
val builder = StringBuilder()
3434
builder.append("MonoStatementItem {\n")
3535
builder.append("\tid: $id\n")
36-
builder.append("\tdesc: $description\n")
36+
builder.append("\tdesc: ${description.replaceNewLines()}\n")
3737
builder.append("\tamount: $amount\n")
3838
builder.append("\tmcc:$mcc (${MCC.mapRussian.getOrDefault(mcc, "unknown")})\n")
3939
builder.append("\ttime: $time\n")

src/main/kotlin/com/github/smaugfm/telegram/TelegramApi.kt

+2-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import kotlinx.coroutines.Job
99
import kotlinx.coroutines.future.asDeferred
1010
import kotlinx.coroutines.launch
1111
import mu.KotlinLogging
12-
import java.net.URI
1312
import kotlin.coroutines.CoroutineContext
1413

1514
private val logger = KotlinLogging.logger {}
@@ -18,7 +17,6 @@ class TelegramApi(
1817
botUsername: String,
1918
botToken: String,
2019
val allowedChatIds: Set<Int>,
21-
@Suppress("UNUSED_PARAMETER") webhookUrl: URI? = null,
2220
) {
2321
private val bot: Bot =
2422
Bot.createPolling(botUsername, botToken)
@@ -45,7 +43,7 @@ class TelegramApi(
4543
}.await()
4644
}
4745

48-
suspend fun answerCallbackQuery(id: String, text: String?) {
46+
suspend fun answerCallbackQuery(id: String, text: String? = null) {
4947
bot.answerCallbackQuery(id, text = text).asDeferred().await()
5048
}
5149

@@ -60,7 +58,7 @@ class TelegramApi(
6058
return@onCallbackQuery
6159

6260
it.data?.let { data ->
63-
dispatcher(Event.Telegram.CallbackQueryReceived(it.id, data))
61+
dispatcher(Event.Telegram.CallbackQueryReceived(it.id, data, it.message?.text!!))
6462
} ?: logger.error("Received callback query without callback_data.\n$it")
6563
}
6664

0 commit comments

Comments
 (0)