mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI-examples.git
synced 2026-05-08 17:40:02 +00:00
Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9c131d5e1 | |||
| 89e1eec53e | |||
| 00ab078891 | |||
| bd71e642b2 | |||
| 514d9d68b8 | |||
| 9746a068b7 | |||
| 9903e0e323 | |||
| 8268cd9bf4 | |||
| b4e2d52e7e | |||
| f829ce7281 | |||
| 20b2ae8175 | |||
| 29ad52b506 | |||
|
|
b848c6bfad | ||
| 6642b95af2 | |||
| 828ab43317 | |||
| 1a4533221c | |||
| e304a5ecab | |||
| 600ac8ebbf | |||
| 07403546f4 | |||
| 9d4b7b5a50 | |||
| e1f5e40143 | |||
| 186a0f7abf | |||
| e660f06edf | |||
| fb6ed8b7ae | |||
| 9981e82a10 | |||
| bef86042f9 | |||
| d1791b3058 | |||
| 0432611f85 | |||
| 6b27aa01fb | |||
| 523e428bcb | |||
| 0e8714cf2b | |||
| ea74f884bf | |||
| 69cbc257b5 | |||
| d3e6014e06 | |||
| 1d260f82e9 | |||
| 5d0b48c4b7 | |||
| 34be1a25b2 | |||
| 990614e257 | |||
| 0d1bcf05fd | |||
| 7d5cb58a3f | |||
| 261df14412 | |||
| 81ba5831c3 | |||
| 0b9c715e25 | |||
| 0216919145 | |||
| e2d56a4d80 | |||
| 70aca52960 | |||
| 6c0d961339 | |||
|
|
a3cdf693f2 | ||
|
|
e378c6630c | ||
| 707ad9a160 | |||
| 68e9830a8f | |||
| 55ebdeadbc | |||
|
|
d4f3d4bc68 | ||
| b3d06c9773 | |||
| e6e3eabf97 | |||
| 47efedf311 | |||
| 8423b1377b | |||
| d0029603ce | |||
| 8d8fa74779 | |||
| 459a70c47b | |||
| 88102f3afa | |||
| a621058fdd | |||
| 56e072aabe | |||
| 73f05bbcd7 | |||
| f053013360 | |||
| bc39279c6c | |||
| ad8fa92e87 | |||
| b0554adb7f | |||
| ad90180def | |||
| 69eda92bc7 | |||
| aee070c6c6 | |||
| 36163d5619 | |||
| 92d1c7a402 | |||
| 7ce784d0a2 | |||
| d203d48391 | |||
| 9352bb0090 | |||
| b1bb11d826 | |||
| 349517462e | |||
| 1708cad654 | |||
| f87a9c5c66 | |||
| a7b54e4b63 | |||
|
|
436213492d | ||
|
|
0c2110a71d | ||
| 949fa1a429 | |||
| 97cdd5a95f | |||
|
|
0cb116acef | ||
|
|
a0332c4efd | ||
| f6bce640da | |||
| d22a99da19 | |||
| 467a3a1710 | |||
| 5810bc5930 | |||
| 2cf2c4264e | |||
|
|
3d5c2ee4b8 | ||
| 360c6b4364 | |||
|
|
bb6a0a125a | ||
| 6a61da2eb7 | |||
| 8cd75673f5 | |||
| d294d0ef59 | |||
| 2ab8ccbfdf | |||
| d12e9aa032 | |||
| 76f151586e | |||
| 1c437690e4 | |||
| 222c7ec8ee | |||
| 59778a3add | |||
| 3e20835bc6 | |||
| c3ad2d4319 | |||
| 59fca968d7 | |||
| f03ba5f177 | |||
|
|
855d2c1296 | ||
| 280f5abce0 | |||
|
|
ed81e76ef8 | ||
| 541b76b292 | |||
| 5b580b5a15 | |||
| 86790ee414 | |||
| 0bbe430374 | |||
| b7d53a7410 | |||
| 73064db226 | |||
| a50eda366d | |||
| e34f0ec9d8 | |||
| c2237f7e87 | |||
| 0bbc6a9555 | |||
| d4d8508abf | |||
| 9acb64fda9 | |||
|
|
760ae36207 | ||
| 5c6b1b7171 | |||
| 6e06357541 | |||
| 38f46dfa3b | |||
|
|
e7f7ef16ac | ||
|
|
d100a5a336 | ||
| 5f0f2ce76d | |||
| 14235e7bd4 | |||
| 6eafd89542 | |||
| ed2922045c | |||
| 21ec50c773 | |||
| ab362e8c3b | |||
| 346755b41c | |||
|
|
a601674d71 | ||
|
|
cea610a0f8 | ||
| b6c92f754f | |||
| 023b810d07 | |||
| 0ec543d5c5 | |||
| 777604e5a0 | |||
| 999c33b2f5 | |||
| ca0427bfdd | |||
| a62a14a599 | |||
|
|
3efd3463a3 | ||
| 590f9ec6d8 | |||
| acdbd4d2ea | |||
| d2d913fca8 | |||
| 75726cac89 | |||
| 71b64689d0 | |||
| 5ba2fc5bab | |||
| 51a5bfb81a | |||
| 35e330c016 | |||
| 90d447fbcf | |||
| 2c5da5da9f | |||
| f79e43364a | |||
| f5a9efa3e7 | |||
| b70b6d1e2b | |||
|
|
3f36a04ac2 | ||
| 62b830d31b | |||
| 06459ebc0a | |||
| 673424b234 | |||
|
|
5d156f6708 | ||
|
|
529f4156fd | ||
|
|
7842ac0dac | ||
|
|
358f2d27d3 | ||
|
|
7964dc4eea | ||
| 9fb6570d21 | |||
| f750589fd3 | |||
| 481533bee2 | |||
| a1a4338869 | |||
| d8e5825ccf | |||
| 3a4c0c4226 | |||
| b85d7a697c | |||
| ad57e4142c | |||
| c7068182e3 | |||
| a5740e6315 | |||
| 3a35995bc7 | |||
| 41fc5a9a4c | |||
| 79700f24e5 | |||
| 73c1af15b3 | |||
| aa9ca976f0 | |||
| 238533a350 | |||
| 6f2a8bb0be | |||
| 99232b53d7 | |||
| 30358f7d2f | |||
| 11a97c520a | |||
| d6a6ad8d37 | |||
| c04a367375 | |||
| b660bf5f42 | |||
| 57dd2380cd | |||
| fbb41c7714 | |||
| 9170d30b2f | |||
| 2f3fd2e53b | |||
| 88697fb5a6 | |||
| 578d00cac6 | |||
| 13ecb3f0df | |||
| a008d861da | |||
| 6f3766dff6 | |||
| fda366d820 | |||
| 578887ac63 | |||
| 6a04b3980c | |||
| 984ffb8bae | |||
| 2bcec6487d | |||
| a5e3bfc3fe | |||
| 941afd0902 | |||
| 94c014b308 | |||
| 538cc9d44f | |||
| cb29726487 | |||
| 262ef26239 | |||
| 41efe5e141 | |||
| 05e289975a | |||
| 753d686fab | |||
| 281243c7e5 | |||
| 3609ae6bc2 | |||
| 4f128f3421 | |||
| ada6cd61d7 | |||
|
|
051d647004 | ||
|
|
d21606860a | ||
|
|
93c0fcb5bd | ||
| b1b8d0eb75 | |||
| 2ac23f70ab | |||
| e155373655 | |||
| d842dab5b8 | |||
| 7186d5e624 | |||
|
|
8fefb17599 | ||
| bcf4ae5888 | |||
| 7090db148e | |||
| 7d786f0e06 | |||
| c88f84011f | |||
| b8cc8854ea | |||
| 13470999e8 | |||
| af04a854ef | |||
| 44e86c9349 | |||
| 65c32d97d5 | |||
| 9b7605591e | |||
| 89d5a4f911 | |||
| 53cf212175 | |||
| 28301a92c9 | |||
| f814b11777 | |||
| 9773a74890 | |||
| a81cfaaba9 | |||
|
|
ee599611f3 | ||
| d3d6cd16c6 | |||
| 02c3d3da1a | |||
| 0ad8e61c0c | |||
| 8f80b7e066 | |||
| 48d1077ce4 | |||
| 6922a6d667 | |||
| 676ce0df80 | |||
| d97c2a0562 | |||
| 35e0cb4a46 | |||
| 30f5513f54 | |||
| fff8edde5f | |||
| e28a795796 | |||
| d289c2101d | |||
| 2ce47074d8 | |||
| 281f0840eb | |||
| 34ed962104 | |||
| aa3337bf3a | |||
| 31d29712be | |||
| 88b348376f | |||
| 0d9e295baa | |||
| ea08bac6e8 | |||
| a85fdc227e | |||
| 43482ee94e | |||
| 4addb6c755 | |||
| 7d958b6edb | |||
| 323c21f415 | |||
| 6350581739 | |||
| ea1d40fd05 | |||
| 8cee63a0fb | |||
| d42ef2c6cb | |||
| c7fe90ddd7 | |||
| acb382d3f7 | |||
| 0cfe60fd77 | |||
| 6719b9e17c | |||
| 8d33dc0ab2 | |||
| 3e2ccf9cf1 | |||
| eccbe71e68 | |||
| 24c74f3b1a | |||
| d7a7e7153e | |||
| 0b37acb7a9 | |||
| 3925ef9423 | |||
| c6019b1862 | |||
| 7b996fe1de | |||
| 4e3c186952 | |||
| 8fdf715419 | |||
| fca8704cec | |||
| bf499ee780 | |||
| 9b10749411 | |||
| d3cb8a32ef | |||
| 0f0ad5a1af | |||
| c3bc55a15c | |||
| 253328f49a | |||
| 8ef50537ae | |||
| 7e7bbfaa93 | |||
| f152ede9b5 | |||
| fcedbf30da | |||
|
|
fd030a92e3 | ||
| d54abf0b32 | |||
|
|
877a20188f | ||
| d0151ff048 | |||
| f8f517cfbb | |||
|
|
b3cbbac917 | ||
| 3e85bb4b22 | |||
| 4e0fb1c137 | |||
| d8c90ef377 | |||
| cb84fd0884 | |||
| 4379862c78 | |||
|
|
ac1d812db0 | ||
| f52590868c | |||
| 51c300c734 | |||
| f6082cff30 | |||
| a40c16fe05 | |||
| a7fe62f4af | |||
| b9c745a21e | |||
| 1c2b068a94 | |||
| 51c2cb1b0e | |||
| cfd4e2fcd5 | |||
|
|
76ceeac757 | ||
| 340de11b0a | |||
| 68a59ca5c8 | |||
|
|
cdb8581318 | ||
|
|
8b3a2ac1ed | ||
| a5b925fc59 | |||
| 9ec9f7a68c | |||
| 0f2829945f | |||
| 4eb80ea53c | |||
| 17cff21847 | |||
| 431069d190 | |||
|
|
fdbac78603 | ||
| da73acd379 | |||
|
|
6e3880f152 | ||
| 1ede6e58e6 | |||
| 0e46f176fb | |||
|
|
2bd449b8b8 | ||
| 82f9da0529 | |||
| 78b7d468f2 | |||
|
|
08059f8174 | ||
|
|
16766046d7 | ||
| 91ea20a269 | |||
| 11e280d177 | |||
| a8d4a307ef | |||
| 2bd2328a38 | |||
| 139de35db9 | |||
|
|
5dd22e1da2 | ||
| 4186ab8270 | |||
| 5aa69d7990 | |||
|
|
df952c69b2 | ||
| 9a03a02bac | |||
| 0e7c050e9e | |||
|
|
bdec902b58 | ||
|
|
cc3c87590d | ||
| 910f892b89 | |||
| 8232cb4d62 | |||
| 3b26971152 | |||
| c0019bcbf8 | |||
| c3dcb4d738 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -8,9 +8,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y libcurl4-openssl-dev
|
||||||
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew build --no-daemon
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.kotlin
|
||||||
out/*
|
out/*
|
||||||
*.iml
|
*.iml
|
||||||
target
|
target
|
||||||
@@ -10,3 +11,6 @@ build/
|
|||||||
out/
|
out/
|
||||||
|
|
||||||
kotlin-js-store/
|
kotlin-js-store/
|
||||||
|
|
||||||
|
local.*
|
||||||
|
local.*/
|
||||||
|
|||||||
2
.template/bot/.env
Normal file
2
.template/bot/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
title=$prompt
|
||||||
|
subtitle=Subtitle of {{$title}}
|
||||||
9
.template/bot/{{$title}}/README.md
Normal file
9
.template/bot/{{$title}}/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# {{$title}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
22
.template/bot/{{$title}}/build.gradle
Normal file
22
.template/bot/{{$title}}/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="{{$title}}Kt"
|
||||||
|
{{$subtitle}}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
34
.template/bot/{{$title}}/src/main/kotlin/{{$title}}.kt
Normal file
34
.template/bot/{{$title}}/src/main/kotlin/{{$title}}.kt
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This place can be the playground for your code.
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
208
.template/module_generator.main.kts
Executable file
208
.template/module_generator.main.kts
Executable file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env kotlin
|
||||||
|
/**
|
||||||
|
* Generates files and folders as they have been put in the folder. Envs uses common syntax, but
|
||||||
|
* values may contains {{${'$'}sampleVariable}} parts, where {{${'$'}sampleVariable}} will be replaced with variable value.
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* .env:
|
||||||
|
* sampleVariable=${'$'}prompt # require request from command line
|
||||||
|
* sampleVariable2=just some value
|
||||||
|
* sampleVariable3=${'$'}{sampleVariable}.${'$'}{sampleVariable2}
|
||||||
|
*
|
||||||
|
* Result variables:
|
||||||
|
* sampleVariable=your input in console # lets imagine you typed it
|
||||||
|
* sampleVariable2=just some value
|
||||||
|
* sampleVariable3=your input in console.just some value
|
||||||
|
*
|
||||||
|
* To use these variables in template, you will need to write {{${'$'}sampleVariable}}.
|
||||||
|
* You may use it in text of files as well as in files/folders names.
|
||||||
|
*
|
||||||
|
* Usage: kotlin generator.kts [args] folders...
|
||||||
|
* Args:
|
||||||
|
* -e, --env: Path to file with args for generation; Use "${'$'}prompt" as values to read variable value from console
|
||||||
|
* -o, --outputFolder: Folder where templates should be used. Folder of calling by default
|
||||||
|
* folders: Folders-templates
|
||||||
|
*/
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
val console = System.console()
|
||||||
|
|
||||||
|
fun String.replaceWithVariables(envs: Map<String, String>): String {
|
||||||
|
var currentString = this
|
||||||
|
var changed = false
|
||||||
|
|
||||||
|
do {
|
||||||
|
changed = false
|
||||||
|
envs.forEach { (k, v) ->
|
||||||
|
val previousString = currentString
|
||||||
|
currentString = currentString.replace("{{$${k}}}", v)
|
||||||
|
changed = changed || currentString != previousString
|
||||||
|
}
|
||||||
|
} while (changed)
|
||||||
|
|
||||||
|
return currentString
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestVariable(variableName: String, defaultValue: String?): String {
|
||||||
|
console.printf("Enter value for variable $variableName${defaultValue ?.let { " [$it]" } ?: ""}: ")
|
||||||
|
return console.readLine().ifEmpty { defaultValue } ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readEnvs(content: String, presets: Map<String, String>): Map<String, String> {
|
||||||
|
val initialEnvs = mutableMapOf<String, String>()
|
||||||
|
content.split("\n").forEach {
|
||||||
|
val withoutComment = it.replace(Regex("\\#.*"), "")
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
val (key, value) = withoutComment.split("=")
|
||||||
|
val existsValue = presets[key]
|
||||||
|
if (value == "\$prompt") {
|
||||||
|
initialEnvs[key] = requestVariable(key, existsValue)
|
||||||
|
} else {
|
||||||
|
initialEnvs[key] = requestVariable(key, value.replaceWithVariables(initialEnvs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var i = 0
|
||||||
|
val readEnvs = initialEnvs.toMutableMap()
|
||||||
|
while (i < readEnvs.size) {
|
||||||
|
val key = readEnvs.keys.elementAt(i)
|
||||||
|
val currentValue = readEnvs.getValue(key)
|
||||||
|
val withReplaced = currentValue.replaceWithVariables(readEnvs)
|
||||||
|
var changed = false
|
||||||
|
if (withReplaced != currentValue) {
|
||||||
|
i = 0
|
||||||
|
readEnvs[key] = withReplaced
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return presets + readEnvs
|
||||||
|
}
|
||||||
|
|
||||||
|
var envFile: File? = null
|
||||||
|
var outputFolder: File = File("./") // current folder by default
|
||||||
|
val templatesFolders = mutableListOf<File>()
|
||||||
|
var extensions: List<String>? = null
|
||||||
|
|
||||||
|
fun readParameters() {
|
||||||
|
var i = 0
|
||||||
|
while (i < args.size) {
|
||||||
|
val arg = args[i]
|
||||||
|
when (arg) {
|
||||||
|
"--env",
|
||||||
|
"-e" -> {
|
||||||
|
i++
|
||||||
|
envFile = File(args[i])
|
||||||
|
}
|
||||||
|
"--extensions",
|
||||||
|
"-ex" -> {
|
||||||
|
i++
|
||||||
|
extensions = args[i].split(",")
|
||||||
|
}
|
||||||
|
"--outputFolder",
|
||||||
|
"-o" -> {
|
||||||
|
i++
|
||||||
|
outputFolder = File(args[i])
|
||||||
|
}
|
||||||
|
"--help",
|
||||||
|
"-h" -> {
|
||||||
|
println("""
|
||||||
|
Generates files and folders as the have been put in the folder. Envs uses common syntax, but
|
||||||
|
values may contains {{${'$'}sampleVariable}} parts, where {{${'$'}sampleVariable}} will be replaced with variable value.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.env:
|
||||||
|
sampleVariable=${'$'}prompt # require request from command line
|
||||||
|
sampleVariable2=just some value
|
||||||
|
sampleVariable3=${'$'}{sampleVariable}.${'$'}{sampleVariable2}
|
||||||
|
|
||||||
|
Result variables:
|
||||||
|
sampleVariable=your input in console # lets imagine you typed it
|
||||||
|
sampleVariable2=just some value
|
||||||
|
sampleVariable3=your input in console.just some value
|
||||||
|
|
||||||
|
To use these variables in template, you will need to write {{${'$'}sampleVariable}}.
|
||||||
|
You may use it in text of files as well as in files/folders names.
|
||||||
|
|
||||||
|
Usage: kotlin generator.kts [args] folders...
|
||||||
|
Args:
|
||||||
|
-e, --env: Path to file with args for generation; Use "${'$'}prompt" as values to read variable value from console
|
||||||
|
-o, --outputFolder: Folder where templates should be used. Folder of calling by default
|
||||||
|
folders: Folders-templates
|
||||||
|
""".trimIndent())
|
||||||
|
Runtime.getRuntime().exit(0)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val potentialFile = File(arg)
|
||||||
|
println("Potential file/folder as template: ${potentialFile.absolutePath}")
|
||||||
|
runCatching {
|
||||||
|
if (potentialFile.exists()) {
|
||||||
|
println("Adding file/folder as template: ${potentialFile.absolutePath}")
|
||||||
|
templatesFolders.add(potentialFile)
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
println("Unable to use folder $arg as template folder")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readParameters()
|
||||||
|
|
||||||
|
val envs: MutableMap<String, String> = envFile ?.let { readEnvs(it.readText(), emptyMap()) } ?.toMutableMap() ?: mutableMapOf()
|
||||||
|
|
||||||
|
println(
|
||||||
|
"""
|
||||||
|
Result environments:
|
||||||
|
${envs.toList().joinToString("\n ") { (k, v) -> "$k=$v" }}
|
||||||
|
Result extensions:
|
||||||
|
${extensions ?.joinToString()}
|
||||||
|
Input folders:
|
||||||
|
${templatesFolders.joinToString("\n ") { it.absolutePath }}
|
||||||
|
Output folder:
|
||||||
|
${outputFolder.absolutePath}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun File.handleTemplate(targetFolder: File, envs: Map<String, String>) {
|
||||||
|
println("Handling $absolutePath")
|
||||||
|
val localEnvs = File(absolutePath, ".env").takeIf { it.exists() } ?.let {
|
||||||
|
println("Reading .env in ${absolutePath}")
|
||||||
|
readEnvs(it.readText(), envs)
|
||||||
|
} ?: envs
|
||||||
|
println(
|
||||||
|
"""
|
||||||
|
Local environments:
|
||||||
|
${localEnvs.toList().joinToString("\n ") { (k, v) -> "$k=$v" }}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val newName = name.replaceWithVariables(localEnvs)
|
||||||
|
println("New name $newName")
|
||||||
|
when {
|
||||||
|
!exists() -> return
|
||||||
|
isFile -> {
|
||||||
|
val content = useLines {
|
||||||
|
it.map { it.replaceWithVariables(localEnvs) }.toList()
|
||||||
|
}.joinToString("\n")
|
||||||
|
val targetFile = File(targetFolder, newName)
|
||||||
|
targetFile.writeText(content)
|
||||||
|
println("Target file: ${targetFile.absolutePath}")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val folder = File(targetFolder, newName)
|
||||||
|
println("Target folder: ${folder.absolutePath}")
|
||||||
|
folder.mkdirs()
|
||||||
|
listFiles() ?.forEach { fileOrFolder ->
|
||||||
|
fileOrFolder.handleTemplate(folder, localEnvs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templatesFolders.forEach { folderOrFile ->
|
||||||
|
folderOrFile.handleTemplate(outputFolder, envs)
|
||||||
|
}
|
||||||
42
BoostsInfoBot/README.md
Normal file
42
BoostsInfoBot/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# BoostsInfoBot
|
||||||
|
|
||||||
|
A bot that retrieves and displays the boost information for a chat.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
On `/start`, the bot sends a reply keyboard with a *Request Channel* button. When the user selects
|
||||||
|
a channel, the bot calls `getUserChatBoosts` and replies with a formatted list of all active boosts
|
||||||
|
for that user in the selected chat, including the start and expiration dates of each boost.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Sample | Description |
|
||||||
|
|----------|-------|--------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Sends the channel-request keyboard |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Reply keyboard with a `RequestChat` button configured for channels
|
||||||
|
- Retrieves user boost list via `getUserChatBoosts`
|
||||||
|
- Formats each boost with its add date and expiration date
|
||||||
|
- Handles `ChatShared` service messages to extract the target chat ID
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
BoostsInfoBot/build.gradle
Normal file
21
BoostsInfoBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="BoostsInfoKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
72
BoostsInfoBot/src/main/kotlin/BoostsInfo.kt
Normal file
72
BoostsInfoBot/src/main/kotlin/BoostsInfo.kt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.get.getUserChatBoosts
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatBoostUpdated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatShared
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatReplyKeyboard
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.requestChannelButton
|
||||||
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
|
import korlibs.time.DateFormat
|
||||||
|
import korlibs.time.format
|
||||||
|
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
val isDebug = args.getOrNull(1) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestChatId = RequestId(1)
|
||||||
|
|
||||||
|
val bot = telegramBot(args.first())
|
||||||
|
|
||||||
|
bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) {
|
||||||
|
onChatBoostUpdated {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("start") {
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
replyMarkup = flatReplyKeyboard {
|
||||||
|
requestChannelButton(
|
||||||
|
"Click me :)",
|
||||||
|
requestChatId,
|
||||||
|
botIsMember = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
regular("Select chat to get know about your boosts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatShared(initialFilter = { it.chatEvent.requestId == requestChatId }) {
|
||||||
|
val boostsInfoContrainer = runCatching {
|
||||||
|
getUserChatBoosts(it.chatEvent.chatId, it.chat.id)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
reply(it) {
|
||||||
|
when {
|
||||||
|
boostsInfoContrainer == null -> +"Unable to take info about boosts in shared chat"
|
||||||
|
boostsInfoContrainer.boosts.isEmpty() -> +"There is no any boosts in passed chat"
|
||||||
|
else -> {
|
||||||
|
boostsInfoContrainer.boosts.forEach {
|
||||||
|
regular("Boost added: ${DateFormat.FORMAT1.format(it.addDate.asDate)}; Boost expire: ${DateFormat.FORMAT1.format(it.expirationDate.asDate)}; Unformatted: $it") + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
58
BusinessConnectionsBot/README.md
Normal file
58
BusinessConnectionsBot/README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# BusinessConnectionsBot
|
||||||
|
|
||||||
|
A comprehensive bot that demonstrates the Telegram Business Account API, including message
|
||||||
|
management, profile editing, star transfers, story posting, and gift listing.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
The bot connects to a business account. When a business connection is established it maps the
|
||||||
|
business chat ID to the owner's personal chat so that management commands can be used in the
|
||||||
|
personal chat. Messages received via the business connection are forwarded to the owner.
|
||||||
|
Typing `PIN` or `UNPIN` in a business message pins or unpins it. A wide set of management commands
|
||||||
|
is available in the owner's PM.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/get_business_account_info` | Print account name, username, bio, and other details |
|
||||||
|
| `/set_business_account_name` | Set the account's first and last name (prompts for input) |
|
||||||
|
| `/set_business_account_username` | Set the account's username (prompts for input) |
|
||||||
|
| `/set_business_account_bio` | Set the account bio (auto-resets to the old value after 15 seconds) |
|
||||||
|
| `/set_business_account_profile_photo` | Set a private profile photo (send a photo in reply) |
|
||||||
|
| `/set_business_account_profile_photo_public` | Set a public profile photo (send a photo in reply) |
|
||||||
|
| `/get_business_account_star_balance` | Show the current star balance of the business account |
|
||||||
|
| `/transfer_business_account_stars` | Transfer stars from the business account to the bot |
|
||||||
|
| `/get_business_account_gifts` | List all gifts received by the business account |
|
||||||
|
| `/post_story` | Post a story with a link area (send a photo in reply) |
|
||||||
|
| `/delete_story` | Delete the most recently posted story |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- `BusinessConnection` event handling: maps business chat IDs to personal owner chats
|
||||||
|
- Forwards business messages to the owner's PM
|
||||||
|
- PIN / UNPIN keyword detection to pin or unpin messages in the business chat
|
||||||
|
- Business message deletion tracking
|
||||||
|
- Mutex-protected concurrent access to the chat mapping
|
||||||
|
- Story creation with `InputStoryContentPhoto` and `StoryAreaTypeLink`
|
||||||
|
- Checklist content support
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
BusinessConnectionsBot/build.gradle
Normal file
21
BusinessConnectionsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="BusinessConnectionsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
503
BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt
Normal file
503
BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.common.Percentage
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.deleteBusinessMessages
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGifts
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.readBusinessMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.removeBusinessAccountProfilePhoto
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountBio
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountName
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountProfilePhoto
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountUsername
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.transferBusinessAccountStars
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.modify.unpinChatMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.get.getBusinessConnection
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.stories.deleteStory
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.stories.postStory
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.commonMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extendedPrivateChatOrThrow
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.flushAccumulatedUpdates
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.requests.abstracts.multipartFile
|
||||||
|
import dev.inmo.tgbotapi.requests.business_connection.InputProfilePhoto
|
||||||
|
import dev.inmo.tgbotapi.requests.stories.PostStory
|
||||||
|
import dev.inmo.tgbotapi.types.ChatId
|
||||||
|
import dev.inmo.tgbotapi.types.MessageId
|
||||||
|
import dev.inmo.tgbotapi.types.RawChatId
|
||||||
|
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
|
||||||
|
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.PhotoContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.StoryContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.VideoContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.VisualMediaGroupPartContent
|
||||||
|
import dev.inmo.tgbotapi.types.stories.InputStoryContent
|
||||||
|
import dev.inmo.tgbotapi.types.stories.StoryArea
|
||||||
|
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
|
||||||
|
import dev.inmo.tgbotapi.types.stories.StoryAreaType
|
||||||
|
import dev.inmo.tgbotapi.utils.botCommand
|
||||||
|
import dev.inmo.tgbotapi.utils.code
|
||||||
|
import dev.inmo.tgbotapi.utils.extensions.splitForText
|
||||||
|
import dev.inmo.tgbotapi.utils.row
|
||||||
|
import korlibs.time.seconds
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.getOrNull(1) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>()
|
||||||
|
val chatsBusinessConnections = mutableMapOf<ChatId, BusinessConnectionId>()
|
||||||
|
val businessConnectionsChatsMutex = Mutex()
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
flushAccumulatedUpdates()
|
||||||
|
|
||||||
|
onBusinessConnectionEnabled {
|
||||||
|
businessConnectionsChatsMutex.withLock {
|
||||||
|
businessConnectionsChats[it.id] = it.userChatId
|
||||||
|
chatsBusinessConnections[it.userChatId] = it.id
|
||||||
|
}
|
||||||
|
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been enabled")
|
||||||
|
}
|
||||||
|
onBusinessConnectionDisabled {
|
||||||
|
businessConnectionsChatsMutex.withLock {
|
||||||
|
businessConnectionsChats.remove(it.id)
|
||||||
|
chatsBusinessConnections.remove(it.userChatId)
|
||||||
|
}
|
||||||
|
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentMessage {
|
||||||
|
it.ifBusinessContentMessage { businessContentMessage ->
|
||||||
|
if (businessContentMessage.content.textContentOrNull() ?.text ?.startsWith("/pin") == true) {
|
||||||
|
businessContentMessage.replyTo ?.ifAccessibleMessage {
|
||||||
|
pinChatMessage(it)
|
||||||
|
return@ifBusinessContentMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (businessContentMessage.content.textContentOrNull() ?.text ?.startsWith("/unpin") == true) {
|
||||||
|
businessContentMessage.replyTo ?.ifAccessibleMessage {
|
||||||
|
unpinChatMessage(it)
|
||||||
|
return@ifBusinessContentMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val sent = execute(it.content.createResend(businessContentMessage.from.id))
|
||||||
|
if (businessContentMessage.sentByBusinessConnectionOwner) {
|
||||||
|
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
|
||||||
|
} else {
|
||||||
|
reply(
|
||||||
|
to = sent,
|
||||||
|
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
chatId = businessConnectionsChats[it.businessConnectionId] ?: return@ifBusinessContentMessage,
|
||||||
|
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
|
||||||
|
replyMarkup = inlineKeyboard {
|
||||||
|
row {
|
||||||
|
dataButton("Read message", "read ${it.chat.id.chatId.long} ${it.messageId.long}")
|
||||||
|
dataButton("Delete message", "delete ${it.chat.id.chatId.long} ${it.messageId.long}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onEditedContentMessage {
|
||||||
|
it.ifBusinessContentMessage { businessContentMessage ->
|
||||||
|
val sent = execute(businessContentMessage.content.createResend(businessContentMessage.from.id))
|
||||||
|
if (businessContentMessage.sentByBusinessConnectionOwner) {
|
||||||
|
reply(sent, "You have edited this message in the ${businessContentMessage.businessConnectionId.string} related chat")
|
||||||
|
} else {
|
||||||
|
reply(sent, "User have edited this message to you in the ${businessContentMessage.businessConnectionId.string} related chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBusinessMessagesDeleted {
|
||||||
|
var businessConnectionOwnerChat = businessConnectionsChatsMutex.withLock {
|
||||||
|
businessConnectionsChats[it.businessConnectionId]
|
||||||
|
}
|
||||||
|
if (businessConnectionOwnerChat == null) {
|
||||||
|
val businessConnection = getBusinessConnection(it.businessConnectionId)
|
||||||
|
businessConnectionsChatsMutex.withLock {
|
||||||
|
businessConnectionsChats[businessConnection.businessConnectionId] = businessConnection.userChatId
|
||||||
|
}
|
||||||
|
businessConnectionOwnerChat = businessConnection.userChatId
|
||||||
|
}
|
||||||
|
send(businessConnectionOwnerChat, "There are several removed messages in chat ${it.chat.id}: ${it.messageIds}")
|
||||||
|
}
|
||||||
|
onCommand("get_business_account_info", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id]
|
||||||
|
val businessConnectionInfo = businessConnectionId ?.let { getBusinessConnection(it) }
|
||||||
|
reply(it) {
|
||||||
|
if (businessConnectionInfo == null) {
|
||||||
|
+"There is no business connection for current chat"
|
||||||
|
} else {
|
||||||
|
+(Json { prettyPrint = true; encodeDefaults = true }.encodeToString(businessConnectionInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMessageDataCallbackQuery(Regex("read \\d+ \\d+")) {
|
||||||
|
val (_, chatIdString, messageIdString) = it.data.split(" ")
|
||||||
|
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
|
||||||
|
|
||||||
|
val readResponse = businessConnectionId ?.let { readBusinessMessage(it, chatId, messageId) }
|
||||||
|
answer(
|
||||||
|
it,
|
||||||
|
if (readResponse == null) {
|
||||||
|
"There is no business connection for current chat"
|
||||||
|
} else {
|
||||||
|
"Message has been read"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onMessageDataCallbackQuery(Regex("delete \\d+ \\d+")) {
|
||||||
|
val (_, chatIdString, messageIdString) = it.data.split(" ")
|
||||||
|
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
|
||||||
|
|
||||||
|
val readResponse = businessConnectionId ?.let { deleteBusinessMessages(it, listOf(messageId)) }
|
||||||
|
answer(
|
||||||
|
it,
|
||||||
|
if (readResponse == null) {
|
||||||
|
"There is no business connection for current chat"
|
||||||
|
} else {
|
||||||
|
"Message has been deleted"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
|
||||||
|
val firstName = args[0]
|
||||||
|
val secondName = args.getOrNull(1)
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
|
||||||
|
val set = runCatching {
|
||||||
|
setBusinessAccountName(
|
||||||
|
businessConnectionId,
|
||||||
|
firstName,
|
||||||
|
secondName
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse { false }
|
||||||
|
reply(it) {
|
||||||
|
if (set) {
|
||||||
|
+"Account name has been set"
|
||||||
|
} else {
|
||||||
|
+"Account name has not been set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommandWithArgs("set_business_account_username", initialFilter = { it.chat is PrivateChat }) { it, args ->
|
||||||
|
val username = args[0]
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
|
||||||
|
val set = runCatching {
|
||||||
|
setBusinessAccountUsername(
|
||||||
|
businessConnectionId,
|
||||||
|
username
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (set) {
|
||||||
|
+"Account username has been set"
|
||||||
|
} else {
|
||||||
|
+"Account username has not been set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("get_business_account_star_balance", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||||
|
val starAmount = runCatching {
|
||||||
|
getBusinessAccountStarBalance(businessConnectionId)
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (starAmount != null) {
|
||||||
|
+"Account stars amount: $starAmount"
|
||||||
|
} else {
|
||||||
|
+"Account stars amount has not been got"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommandWithArgs("transfer_business_account_stars", initialFilter = { it.chat is PrivateChat }) { it, args ->
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
|
||||||
|
val count = args.firstOrNull() ?.toIntOrNull() ?: reply(it) {
|
||||||
|
"Pass amount of stars to transfer to bot with command"
|
||||||
|
}.let {
|
||||||
|
return@onCommandWithArgs
|
||||||
|
}
|
||||||
|
val transferred = runCatching {
|
||||||
|
transferBusinessAccountStars(businessConnectionId, count)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (transferred) {
|
||||||
|
+"Stars have been transferred"
|
||||||
|
} else {
|
||||||
|
+"Stars have not been transferred"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("get_business_account_gifts", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||||
|
val giftsFlow = runCatching {
|
||||||
|
getBusinessAccountGiftsFlow(businessConnectionId)
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (giftsFlow == null) {
|
||||||
|
reply(it) {
|
||||||
|
+"Error in receiving of gifts"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
giftsFlow.collect { giftsPage ->
|
||||||
|
giftsPage.gifts.joinToString {
|
||||||
|
it.toString()
|
||||||
|
}.splitForText().forEach { message ->
|
||||||
|
reply(it, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("set_business_account_bio", requireOnlyCommandInMessage = false, initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val initialBio = getChat(it.chat).extendedPrivateChatOrThrow().bio
|
||||||
|
val bio = it.content.text.removePrefix("/set_business_account_bio").trim()
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||||
|
val set = runCatching {
|
||||||
|
setBusinessAccountBio(
|
||||||
|
businessConnectionId,
|
||||||
|
bio
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (set) {
|
||||||
|
+"Account bio has been set. It will be reset within 15 seconds.\n\nInitial bio: " + code(initialBio)
|
||||||
|
} else {
|
||||||
|
+"Account bio has not been set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(15.seconds)
|
||||||
|
val reset = runCatching {
|
||||||
|
setBusinessAccountBio(
|
||||||
|
businessConnectionId,
|
||||||
|
initialBio
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (reset) {
|
||||||
|
+"Account bio has been reset"
|
||||||
|
} else {
|
||||||
|
+"Account bio has not been set. Set it manually: " + code(initialBio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend fun handleSetProfilePhoto(it: CommonMessage<TextContent>, isPublic: Boolean) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
|
||||||
|
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
|
||||||
|
if (replyTo == null) {
|
||||||
|
reply(it) {
|
||||||
|
+"Reply to photo for using of this command"
|
||||||
|
}
|
||||||
|
return@handleSetProfilePhoto
|
||||||
|
}
|
||||||
|
|
||||||
|
val set = runCatching {
|
||||||
|
val file = downloadFileToTemp(replyTo.content)
|
||||||
|
setBusinessAccountProfilePhoto(
|
||||||
|
businessConnectionId,
|
||||||
|
InputProfilePhoto.Static(
|
||||||
|
file.multipartFile()
|
||||||
|
),
|
||||||
|
isPublic = isPublic
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (set) {
|
||||||
|
+"Account profile photo has been set. It will be reset within 15 seconds"
|
||||||
|
} else {
|
||||||
|
+"Account profile photo has not been set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (set == false) { return@handleSetProfilePhoto }
|
||||||
|
delay(15.seconds)
|
||||||
|
val reset = runCatching {
|
||||||
|
removeBusinessAccountProfilePhoto(
|
||||||
|
businessConnectionId,
|
||||||
|
isPublic = isPublic
|
||||||
|
)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (reset) {
|
||||||
|
+"Account profile photo has been reset"
|
||||||
|
} else {
|
||||||
|
+"Account profile photo has not been set. Set it manually"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("set_business_account_profile_photo", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
handleSetProfilePhoto(it, false)
|
||||||
|
}
|
||||||
|
onCommand("set_business_account_profile_photo_public", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
handleSetProfilePhoto(it, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||||
|
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
|
||||||
|
if (replyTo == null) {
|
||||||
|
reply(it) {
|
||||||
|
+"Reply to photo or video for using of this command"
|
||||||
|
}
|
||||||
|
return@onCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
val posted = runCatching {
|
||||||
|
val file = downloadFileToTemp(replyTo.content)
|
||||||
|
postStory(
|
||||||
|
businessConnectionId,
|
||||||
|
when (replyTo.content) {
|
||||||
|
is PhotoContent -> InputStoryContent.Photo(
|
||||||
|
file.multipartFile()
|
||||||
|
)
|
||||||
|
is VideoContent -> InputStoryContent.Video(
|
||||||
|
file.multipartFile()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
|
||||||
|
areas = listOf(
|
||||||
|
StoryArea(
|
||||||
|
StoryAreaPosition(
|
||||||
|
x = Percentage.of100(50.0),
|
||||||
|
y = Percentage.of100(50.0),
|
||||||
|
width = Percentage.of100(8.0),
|
||||||
|
height = Percentage.of100(8.0),
|
||||||
|
rotationAngle = 45.0,
|
||||||
|
cornerRadius = Percentage.of100(4.0),
|
||||||
|
),
|
||||||
|
StoryAreaType.Link(
|
||||||
|
"https://github.com/InsanusMokrassar/TelegramBotAPI-examples/blob/master/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
+"It is test of postStory :)"
|
||||||
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (posted != null) {
|
||||||
|
+"Story has been posted. You may unpost it with " + botCommand("remove_story")
|
||||||
|
} else {
|
||||||
|
+"Story has not been posted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
|
||||||
|
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||||
|
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<StoryContent>()
|
||||||
|
if (replyTo == null) {
|
||||||
|
reply(it) {
|
||||||
|
+"Reply to photo or video for using of this command"
|
||||||
|
}
|
||||||
|
return@onCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
val deleted = runCatching {
|
||||||
|
deleteStory(businessConnectionId, replyTo.content.story.id)
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
it.printStackTrace()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
reply(it) {
|
||||||
|
if (deleted) {
|
||||||
|
+"Story has been deleted"
|
||||||
|
} else {
|
||||||
|
+"Story has not been deleted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will work when some premium user sending to some other user checklist
|
||||||
|
onChecklistContent {
|
||||||
|
execute(
|
||||||
|
it.content.createResend(
|
||||||
|
it.chat.id,
|
||||||
|
businessConnectionId = it.chat.id.businessConnectionId ?: chatsBusinessConnections[it.chat.id] ?: return@onChecklistContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -1,9 +1,41 @@
|
|||||||
# ChatAvatarSetter
|
# ChatAvatarSetter
|
||||||
|
|
||||||
This bot will set the chat avatar based on the image sent to bot
|
A bot that updates a group or channel's avatar using a photo sent to the bot.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
When the bot receives a photo message, it downloads the highest-resolution version of the photo
|
||||||
|
and sets it as the chat photo for the chat the message was sent from. If the operation fails (e.g.,
|
||||||
|
due to missing admin rights), the bot sends an error message back to the user.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Downloads the largest available photo size from the incoming message
|
||||||
|
- Calls `setChatPhoto` to apply the downloaded image as the chat's avatar
|
||||||
|
- Returns a user-facing error message if the update fails
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
../gradlew run --args="BOT_TOKEN"
|
../gradlew run --args="BOT_TOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note:** The bot must be an administrator with *Change group info* permission in the target chat.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
import dev.inmo.tgbotapi.extensions.api.chat.modify.setChatPhoto
|
import dev.inmo.tgbotapi.extensions.api.chat.modify.setChatPhoto
|
||||||
@@ -5,9 +6,9 @@ import dev.inmo.tgbotapi.extensions.api.files.downloadFile
|
|||||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
||||||
import dev.inmo.tgbotapi.extensions.utils.*
|
|
||||||
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun main(args: Array<String>) {
|
||||||
val bot = telegramBot(args.first())
|
val bot = telegramBot(args.first())
|
||||||
@@ -15,17 +16,13 @@ suspend fun main(args: Array<String>) {
|
|||||||
bot.buildBehaviourWithLongPolling(scope = CoroutineScope(Dispatchers.IO)) {
|
bot.buildBehaviourWithLongPolling(scope = CoroutineScope(Dispatchers.IO)) {
|
||||||
onPhoto {
|
onPhoto {
|
||||||
val bytes = downloadFile(it.content)
|
val bytes = downloadFile(it.content)
|
||||||
runCatchingSafely {
|
runCatchingLogging {
|
||||||
setChatPhoto(
|
setChatPhoto(
|
||||||
it.chat.id,
|
it.chat.id,
|
||||||
bytes.asMultipartFile("sample.jpg")
|
bytes.asMultipartFile("sample.jpg")
|
||||||
)
|
)
|
||||||
}.onSuccess { b ->
|
}.onSuccess { _ ->
|
||||||
if (b) {
|
|
||||||
reply(it, "Done")
|
reply(it, "Done")
|
||||||
} else {
|
|
||||||
reply(it, "Something went wrong")
|
|
||||||
}
|
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
|
|||||||
42
ChecklistsBot/README.md
Normal file
42
ChecklistsBot/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# ChecklistsBot
|
||||||
|
|
||||||
|
A bot that handles Telegram premium checklist messages and tracks task completion events.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Listens for messages containing a checklist. When a checklist message is received, the bot sends
|
||||||
|
a formatted reply showing all tasks with their completion status. It also reacts to task-level
|
||||||
|
events: when a task is marked as done or a new task is added to an existing checklist, the bot
|
||||||
|
sends an update reply referencing the affected task.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Detects `ChecklistContent` messages (Telegram Premium feature)
|
||||||
|
- Formats checklist tasks with ✅ (completed) and ⬜ (pending) indicators
|
||||||
|
- Handles `ChecklistTasksDone` events — replies when a task is marked complete
|
||||||
|
- Handles `ChecklistTasksAdded` events — replies when new tasks are appended
|
||||||
|
- Uses rich text message building for formatted output
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
ChecklistsBot/build.gradle
Normal file
21
ChecklistsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="ChecklistsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
120
ChecklistsBot/src/main/kotlin/ChecklistsBot.kt
Normal file
120
ChecklistsBot/src/main/kotlin/ChecklistsBot.kt
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.resend
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.suggested.approveSuggestedPost
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostApproved
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostDeclined
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChannelDirectMessagesConfigurationChanged
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistContent
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistTasksAdded
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistTasksDone
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostApprovalFailed
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostApproved
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostDeclined
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostPaid
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostRefunded
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.channelDirectMessagesContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
|
||||||
|
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.ChecklistContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
||||||
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
|
import dev.inmo.tgbotapi.utils.bold
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
|
import dev.inmo.tgbotapi.utils.code
|
||||||
|
import dev.inmo.tgbotapi.utils.firstOf
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.Default),
|
||||||
|
testServer = isTestServer,
|
||||||
|
) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
fun ChecklistContent.textBuilderTextSources(): TextSourcesList {
|
||||||
|
return buildEntities {
|
||||||
|
+checklist.textSources + "\n\n"
|
||||||
|
checklist.tasks.forEach { task ->
|
||||||
|
+"• "
|
||||||
|
code(
|
||||||
|
if (task.completionDate != null) {
|
||||||
|
"[x] "
|
||||||
|
} else {
|
||||||
|
"[ ] "
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bold(task.textSources) + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChecklistContent { messageWithContent ->
|
||||||
|
reply(messageWithContent) {
|
||||||
|
+messageWithContent.content.textBuilderTextSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChecklistTasksDone { eventMessage ->
|
||||||
|
reply(
|
||||||
|
eventMessage,
|
||||||
|
checklistTaskId = eventMessage.chatEvent.markedAsDone ?.firstOrNull()
|
||||||
|
) {
|
||||||
|
eventMessage.chatEvent.checklistMessage.content.checklist
|
||||||
|
+eventMessage.chatEvent.checklistMessage.content.textBuilderTextSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChecklistTasksAdded { messageWithContent ->
|
||||||
|
reply(
|
||||||
|
messageWithContent.chatEvent.checklistMessage,
|
||||||
|
checklistTaskId = messageWithContent.chatEvent.tasks.firstOrNull() ?.id
|
||||||
|
) {
|
||||||
|
+messageWithContent.chatEvent.checklistMessage.content.textBuilderTextSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
47
CustomBot/README.md
Normal file
47
CustomBot/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# CustomBot
|
||||||
|
|
||||||
|
A bot that demonstrates custom middleware, custom subcontext data, and several utility features
|
||||||
|
of the TelegramBotAPI library.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Shows how to attach a logging middleware to every API request and how to store arbitrary data in
|
||||||
|
a per-update subcontext. Additionally demonstrates retrieving and sending a user's profile audio
|
||||||
|
playlist and querying the bot's own star balance.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Retrieves the sender's profile audio files and sends them as an audio media group |
|
||||||
|
| `/additional_command` | Demo command that accesses and prints custom subcontext data |
|
||||||
|
| `/getMyStarBalance` | Queries and replies with the bot's current Telegram Star balance |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Custom request middleware that logs every outgoing API call
|
||||||
|
- Custom `BehaviourContext` subcontext with arbitrary stored data
|
||||||
|
- Profile audio retrieval via `getUserProfilePhotos`-style API for audio
|
||||||
|
- Audio media group sending (batched uploads)
|
||||||
|
- Star balance query via `getStarTransactions`
|
||||||
|
- Channel direct-message configuration tracking via `ChatBoostUpdated` events
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
CustomBot/build.gradle
Normal file
21
CustomBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="CustomBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
136
CustomBot/src/main/kotlin/CustomBot.kt
Normal file
136
CustomBot/src/main/kotlin/CustomBot.kt
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.get.getUserProfileAudios
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.replyWithAudio
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.replyWithPlaylist
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChannelDirectMessagesConfigurationChanged
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatOwnerChanged
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatOwnerLeft
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
|
||||||
|
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
|
||||||
|
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
private var BehaviourContextData.update: Update?
|
||||||
|
get() = get("update") as? Update
|
||||||
|
set(value) = set("update", value)
|
||||||
|
|
||||||
|
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
||||||
|
get() = get("commonMessage") as? CommonMessage<*>
|
||||||
|
set(value) = set("commonMessage", value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This place can be the playground for your code.
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.IO),
|
||||||
|
testServer = isTestServer,
|
||||||
|
builder = {
|
||||||
|
includeMiddlewares {
|
||||||
|
addMiddleware {
|
||||||
|
doOnRequestReturnResult { result, request, _ ->
|
||||||
|
println("Result of $request:\n\n$result")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subcontextInitialAction = buildSubcontextInitialAction {
|
||||||
|
add {
|
||||||
|
data.update = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
onCommand("start") {
|
||||||
|
println(data.update)
|
||||||
|
println(data.commonMessage)
|
||||||
|
println(getChat(it.chat))
|
||||||
|
var currentOffset = 0
|
||||||
|
val pageSize = 2
|
||||||
|
do {
|
||||||
|
val userAudios = getUserProfileAudios(userId = it.chat.id, offset = currentOffset, limit = pageSize)
|
||||||
|
currentOffset += pageSize
|
||||||
|
println(userAudios)
|
||||||
|
when (userAudios.audios.size) {
|
||||||
|
1 -> {
|
||||||
|
replyWithAudio(
|
||||||
|
it,
|
||||||
|
userAudios.audios.first().fileId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
0 -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
replyWithPlaylist(
|
||||||
|
it,
|
||||||
|
userAudios.audios.map {
|
||||||
|
it.toTelegramMediaAudio()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (currentOffset < userAudios.totalCount && userAudios.audios.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand(
|
||||||
|
"additional_command",
|
||||||
|
additionalSubcontextInitialAction = { update, commonMessage ->
|
||||||
|
data.commonMessage = commonMessage
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
println(data.update)
|
||||||
|
println(data.commonMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("getMyStarBalance") {
|
||||||
|
reply(
|
||||||
|
to = it,
|
||||||
|
text = getMyStarBalance().toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChannelDirectMessagesConfigurationChanged {
|
||||||
|
println(it.chatEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -1,6 +1,37 @@
|
|||||||
# DeepLinksBot
|
# DeepLinksBot
|
||||||
|
|
||||||
This bot will send you deeplink to this bot when you send some text message and react on the `start` button
|
A bot that generates and handles Telegram deep links.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Generates a deep link to the bot when the user sends any text message. When a deep link is followed
|
||||||
|
(i.e., the `/start` command is received with a payload), the bot confirms what payload was received.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Displays a help/welcome message; also handles deep-link payloads |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Requires a registered bot username (validates that `getMe` returns a username)
|
||||||
|
- Generates a `t.me/<username>?start=<payload>` deep link from any incoming text message
|
||||||
|
- Subscribes to deep-link follow events with `waitDeepLinks()` and confirms the received payload
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelySkippingExceptions
|
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onDeepLink
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onText
|
||||||
import dev.inmo.tgbotapi.extensions.utils.formatting.makeTelegramDeepLink
|
import dev.inmo.tgbotapi.extensions.utils.formatting.makeTelegramDeepLink
|
||||||
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
|
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
|
||||||
|
|
||||||
@@ -17,12 +17,17 @@ suspend fun main(vararg args: String) {
|
|||||||
|
|
||||||
telegramBotWithBehaviourAndLongPolling(botToken) {
|
telegramBotWithBehaviourAndLongPolling(botToken) {
|
||||||
val me = bot.getMe()
|
val me = bot.getMe()
|
||||||
|
val username = me.username
|
||||||
println(me)
|
println(me)
|
||||||
|
|
||||||
|
if (username == null) {
|
||||||
|
error("Unable to start bot work: it have no username")
|
||||||
|
}
|
||||||
|
|
||||||
onText(
|
onText(
|
||||||
initialFilter = { it.content.textSources.none { it is BotCommandTextSource } } // excluding messages with commands
|
initialFilter = { it.content.textSources.none { it is BotCommandTextSource } } // excluding messages with commands
|
||||||
) {
|
) {
|
||||||
reply(it, makeTelegramDeepLink(me.username, it.content.text))
|
reply(it, makeTelegramDeepLink(username, it.content.text))
|
||||||
}
|
}
|
||||||
|
|
||||||
onCommand("start", requireOnlyCommandInMessage = true) { // handling of `start` without args
|
onCommand("start", requireOnlyCommandInMessage = true) { // handling of `start` without args
|
||||||
|
|||||||
40
DraftsBot/README.md
Normal file
40
DraftsBot/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# DraftsBot
|
||||||
|
|
||||||
|
A bot that demonstrates the message-draft flow API by progressively revealing text to the user.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
On `/test_draft_flow`, the bot sends a series of draft text updates to the user, each building
|
||||||
|
on the previous one, before committing the final message. This illustrates how to use the draft
|
||||||
|
message API to stream partial content before finalising it.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/test_draft_flow` | Starts a draft-message flow that progressively reveals text |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Uses the `draftFlow` / `sendDraftMessage` API to emit incremental text updates
|
||||||
|
- Demonstrates the difference between draft (editable intermediate state) and final message
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
DraftsBot/build.gradle
Normal file
21
DraftsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="TopicsHandlingKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
86
DraftsBot/src/main/kotlin/DraftsBot.kt
Normal file
86
DraftsBot/src/main/kotlin/DraftsBot.kt
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import com.benasher44.uuid.uuid4
|
||||||
|
import dev.inmo.kslog.common.w
|
||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.bot.TelegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.forum.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlow
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicClosed
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicCreated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicEdited
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicReopened
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGeneralForumTopicHidden
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGeneralForumTopicUnhidden
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPrivateForumTopicCreated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPrivateForumTopicEdited
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.forumChatOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.forumContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.privateChatOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.privateForumChatOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.flushAccumulatedUpdates
|
||||||
|
import dev.inmo.tgbotapi.types.BotCommand
|
||||||
|
import dev.inmo.tgbotapi.types.ForumTopic
|
||||||
|
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
||||||
|
import dev.inmo.tgbotapi.types.commands.BotCommandScope
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
|
const val testText = """
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
"""
|
||||||
|
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
args.first(),
|
||||||
|
CoroutineScope(Dispatchers.Default),
|
||||||
|
defaultExceptionsHandler = {
|
||||||
|
it.printStackTrace()
|
||||||
|
},
|
||||||
|
builder = {
|
||||||
|
client = client.config {
|
||||||
|
install(HttpTimeout) {
|
||||||
|
requestTimeoutMillis = 30000
|
||||||
|
socketTimeoutMillis = 30000
|
||||||
|
connectTimeoutMillis = 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
onCommand("test_draft_flow") {
|
||||||
|
sendMessageDraftFlowWithTexts(
|
||||||
|
it.chat.id,
|
||||||
|
flow<String> {
|
||||||
|
val step = 50
|
||||||
|
var currentLength = step
|
||||||
|
while (isActive && testText.length > currentLength) {
|
||||||
|
delay(500L)
|
||||||
|
emit(testText.take(currentLength))
|
||||||
|
currentLength += step
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
send(it.chat, testText)
|
||||||
|
}
|
||||||
|
|
||||||
|
setMyCommands(
|
||||||
|
BotCommand("test_draft_flow", "Start draft testing with flow"),
|
||||||
|
scope = BotCommandScope.AllGroupChats
|
||||||
|
)
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -1,7 +1,41 @@
|
|||||||
# FSM
|
# FSMBot
|
||||||
|
|
||||||
This bot contains an example of working with FSM included in project
|
A demonstration of the Finite State Machine (FSM) pattern provided by the
|
||||||
[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils)
|
[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) library.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Implements a simple two-state FSM. After `/start` is sent, the bot enters
|
||||||
|
`ExpectContentOrStopState` and re-sends every message it receives back to the user.
|
||||||
|
This continues until the user sends `/stop`, at which point the FSM transitions to
|
||||||
|
`StopState` and content forwarding ends.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Starts the FSM loop — bot begins echoing content back to the user |
|
||||||
|
| `/stop` | Ends the FSM loop — bot stops echoing |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Two-state FSM: `ExpectContentOrStopState` → `StopState`
|
||||||
|
- `ExpectContentOrStopState` uses `expectContentOrCommands()` to filter messages
|
||||||
|
- Erroneous FSM states are caught and handled gracefully
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.AccumulatorFlow
|
import dev.inmo.micro_utils.coroutines.awaitFirst
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
import dev.inmo.micro_utils.fsm.common.State
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.sendMessage
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitAnyContentMessage
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommandMessage
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndFSMAndStartLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.command
|
||||||
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
import dev.inmo.tgbotapi.extensions.utils.formatting.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.containsCommand
|
||||||
import dev.inmo.tgbotapi.types.ChatId
|
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgs
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.sameThread
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
import dev.inmo.tgbotapi.utils.botCommand
|
import dev.inmo.tgbotapi.utils.botCommand
|
||||||
import kotlinx.coroutines.*
|
import dev.inmo.tgbotapi.utils.firstOf
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
sealed interface BotState : State
|
sealed interface BotState : State
|
||||||
data class ExpectContentOrStopState(override val context: ChatId, val sourceMessage: CommonMessage<TextContent>) : BotState
|
data class ExpectContentOrStopState(override val context: IdChatIdentifier, val sourceMessage: CommonMessage<TextContent>) : BotState
|
||||||
data class StopState(override val context: ChatId) : BotState
|
data class StopState(override val context: IdChatIdentifier) : BotState
|
||||||
|
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun main(args: Array<String>) {
|
||||||
val botToken = args.first()
|
val botToken = args.first()
|
||||||
@@ -39,30 +48,57 @@ suspend fun main(args: Array<String>) {
|
|||||||
) {
|
) {
|
||||||
strictlyOn<ExpectContentOrStopState> {
|
strictlyOn<ExpectContentOrStopState> {
|
||||||
send(
|
send(
|
||||||
it.context
|
it.context,
|
||||||
) {
|
) {
|
||||||
+"Send me some content or " + botCommand("stop") + " if you want to stop sending"
|
+"Send me some content or " + botCommand("stop") + " if you want to stop sending"
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentMessage = waitContentMessage().first()
|
val contentMessage = firstOf(
|
||||||
|
{
|
||||||
|
waitCommandMessage("stop").filter { message ->
|
||||||
|
message.sameThread(it.sourceMessage)
|
||||||
|
}.first()
|
||||||
|
null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waitAnyContentMessage().filter { message ->
|
||||||
|
message.sameThread(it.sourceMessage)
|
||||||
|
}.filter {
|
||||||
|
containsCommand(
|
||||||
|
"stop",
|
||||||
|
it.withContentOrNull<TextContent>() ?.content ?.textSources ?: return@filter false
|
||||||
|
) == false
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
) ?: return@strictlyOn StopState(it.context)
|
||||||
|
|
||||||
val content = contentMessage.content
|
val content = contentMessage.content
|
||||||
|
|
||||||
when {
|
|
||||||
content is TextContent && content.parseCommandsWithParams().keys.contains("stop") -> StopState(it.context)
|
|
||||||
else -> {
|
|
||||||
execute(content.createResend(it.context))
|
execute(content.createResend(it.context))
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
strictlyOn<StopState> {
|
strictlyOn<StopState> {
|
||||||
send(it.context) { +"You have stopped sending of content" }
|
send(it.context) { +"You have stopped sending of content" }
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
command("start") {
|
command(
|
||||||
|
"start"
|
||||||
|
) {
|
||||||
startChain(ExpectContentOrStopState(it.chat.id, it))
|
startChain(ExpectContentOrStopState(it.chat.id, it))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onContentMessage(
|
||||||
|
{
|
||||||
|
it.content.textContentOrNull() ?.text == "/start"
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
startChain(ExpectContentOrStopState(it.chat.id, it.withContentOrNull() ?: return@onContentMessage))
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,48 @@
|
|||||||
# FilesLoaderBot
|
# FilesLoaderBot
|
||||||
|
|
||||||
This bot will download incoming files
|
A bot that downloads any media file sent to it and then re-uploads it back to the chat.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
For every message containing a file (photo, video, audio, document, sticker, animation, voice,
|
||||||
|
video note, etc.), the bot downloads the file to a local directory, then sends the file back to
|
||||||
|
the chat. Media groups are expanded and each file is re-sent individually. While processing, the
|
||||||
|
bot sends an appropriate "upload" chat action (e.g., *uploading video*, *uploading photo*).
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
| 2 *(optional)* | `/path/to/dir` | Directory where files are saved (defaults to `/tmp/`) |
|
||||||
|
|
||||||
|
Optional flags (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Sends a usage instruction message |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Supports all Telegram media types: photos, videos, audio, documents, stickers, animations, voice messages, video notes
|
||||||
|
- Handles media groups by iterating over each item and re-uploading it individually
|
||||||
|
- Sends contextually appropriate chat actions during upload (typing, upload_video, upload_audio, etc.)
|
||||||
|
- Logs the local file path after each download
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
../gradlew run --args="BOT_TOKEN[ optional/folder/path]"
|
# Default directory (/tmp/)
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
|
||||||
|
# Custom directory
|
||||||
|
../gradlew run --args="BOT_TOKEN /path/to/save/dir"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||||
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
|
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
import dev.inmo.tgbotapi.extensions.api.send.*
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMedia
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMedia
|
||||||
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
|
import dev.inmo.tgbotapi.types.actions.*
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.*
|
||||||
import dev.inmo.tgbotapi.utils.filenameFromUrl
|
import dev.inmo.tgbotapi.utils.filenameFromUrl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -18,16 +27,90 @@ suspend fun main(args: Array<String>) {
|
|||||||
directoryOrFile.mkdirs()
|
directoryOrFile.mkdirs()
|
||||||
|
|
||||||
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
|
onCommand("start") {
|
||||||
|
reply(it, "Send me any media (like photo or video) to download it")
|
||||||
|
}
|
||||||
onMedia(initialFilter = null) {
|
onMedia(initialFilter = null) {
|
||||||
val pathedFile = bot.getFileAdditionalInfo(it.content.media)
|
val content = it.content
|
||||||
|
val pathedFile = bot.getFileAdditionalInfo(content.media)
|
||||||
val outFile = File(directoryOrFile, pathedFile.filePath.filenameFromUrl)
|
val outFile = File(directoryOrFile, pathedFile.filePath.filenameFromUrl)
|
||||||
|
withTypingAction(it.chat.id) {
|
||||||
runCatching {
|
runCatching {
|
||||||
bot.downloadFile(it.content.media, outFile)
|
bot.downloadFile(content.media, outFile)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
it.printStackTrace()
|
||||||
}
|
}.onSuccess { _ ->
|
||||||
reply(it, "Saved to ${outFile.absolutePath}")
|
reply(it, "Saved to ${outFile.absolutePath}")
|
||||||
}
|
}
|
||||||
onContentMessage { println(it) }
|
}.onSuccess { _ ->
|
||||||
|
val action = when (content) {
|
||||||
|
is PhotoContent -> UploadPhotoAction
|
||||||
|
is AnimationContent,
|
||||||
|
is VideoContent -> UploadVideoAction
|
||||||
|
is StickerContent -> ChooseStickerAction
|
||||||
|
is MediaGroupContent<*> -> UploadPhotoAction
|
||||||
|
is DocumentContent -> UploadDocumentAction
|
||||||
|
is VoiceContent,
|
||||||
|
is AudioContent -> RecordVoiceAction
|
||||||
|
is VideoNoteContent -> UploadVideoNoteAction
|
||||||
|
}
|
||||||
|
withAction(it.chat.id, action) {
|
||||||
|
when (content) {
|
||||||
|
is PhotoContent -> replyWithPhoto(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is AnimationContent -> replyWithAnimation(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is VideoContent -> replyWithVideo(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is StickerContent -> replyWithSticker(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is MediaGroupContent<*> -> replyWithMediaGroup(
|
||||||
|
it,
|
||||||
|
content.group.map {
|
||||||
|
when (val innerContent = it.content) {
|
||||||
|
is AudioContent -> TelegramMediaAudio(
|
||||||
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
|
)
|
||||||
|
is DocumentContent -> TelegramMediaDocument(
|
||||||
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
|
)
|
||||||
|
is PhotoContent -> TelegramMediaPhoto(
|
||||||
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
|
)
|
||||||
|
is VideoContent -> TelegramMediaVideo(
|
||||||
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is AudioContent -> replyWithAudio(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is DocumentContent -> replyWithDocument(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is VoiceContent -> replyWithVoice(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
is VideoNoteContent -> replyWithVideoNote(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,36 @@
|
|||||||
# ForwarderBot
|
# ForwardInfoSenderBot
|
||||||
|
|
||||||
The main purpose of this bot is just to send info about forwarder when bot receive any update
|
A bot that analyses the origin of forwarded messages and prints detailed information about the forwarder.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
For every message that was forwarded to the bot, it inspects the forward metadata and sends back a
|
||||||
|
formatted reply describing who or what originally sent the message: a regular user, a bot, a channel,
|
||||||
|
or an anonymous/hidden sender. Premium status, user IDs, and usernames are included where available.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Identifies forwarder type: regular user, bot, public channel, anonymous group admin, or hidden user
|
||||||
|
- Displays premium user status, numeric IDs, and usernames using `code` and hyperlink entities
|
||||||
|
- Re-sends the original message content alongside the metadata reply
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
import dev.inmo.tgbotapi.extensions.utils.formatting.*
|
import dev.inmo.tgbotapi.extensions.utils.formatting.makeLink
|
||||||
import dev.inmo.tgbotapi.types.chat.CommonBot
|
import dev.inmo.tgbotapi.types.chat.CommonBot
|
||||||
import dev.inmo.tgbotapi.types.chat.CommonUser
|
import dev.inmo.tgbotapi.types.chat.CommonUser
|
||||||
import dev.inmo.tgbotapi.types.chat.ExtendedBot
|
import dev.inmo.tgbotapi.types.chat.ExtendedBot
|
||||||
import dev.inmo.tgbotapi.types.message.*
|
import dev.inmo.tgbotapi.types.message.*
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
import dev.inmo.tgbotapi.utils.code
|
import dev.inmo.tgbotapi.utils.code
|
||||||
|
import dev.inmo.tgbotapi.utils.link
|
||||||
import dev.inmo.tgbotapi.utils.regular
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
@@ -40,7 +42,14 @@ suspend fun main(vararg args: String) {
|
|||||||
is ExtendedBot -> regular("Bot ")
|
is ExtendedBot -> regular("Bot ")
|
||||||
} + code(user.id.chatId.toString()) + " (${user.firstName} ${user.lastName}: ${user.username?.username ?: "Without username"})"
|
} + code(user.id.chatId.toString()) + " (${user.firstName} ${user.lastName}: ${user.username?.username ?: "Without username"})"
|
||||||
}
|
}
|
||||||
is ForwardInfo.PublicChat.FromChannel -> regular("Channel (") + code(forwardInfo.channelChat.title) + ")"
|
is ForwardInfo.PublicChat.FromChannel -> {
|
||||||
|
regular("Channel (") + (forwardInfo.channelChat.username ?.let {
|
||||||
|
link(
|
||||||
|
forwardInfo.channelChat.title,
|
||||||
|
makeLink(it)
|
||||||
|
)
|
||||||
|
} ?: code(forwardInfo.channelChat.title)) + ")"
|
||||||
|
}
|
||||||
is ForwardInfo.PublicChat.FromSupergroup -> regular("Supergroup (") + code(forwardInfo.group.title) + ")"
|
is ForwardInfo.PublicChat.FromSupergroup -> regular("Supergroup (") + code(forwardInfo.group.title) + ")"
|
||||||
is ForwardInfo.PublicChat.SentByChannel -> regular("Sent by channel (") + code(forwardInfo.channelChat.title) + ")"
|
is ForwardInfo.PublicChat.SentByChannel -> regular("Sent by channel (") + code(forwardInfo.channelChat.title) + ")"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# GetMeBot
|
|
||||||
|
|
||||||
This is one of the most easiest bot - it will just print information about itself
|
|
||||||
|
|
||||||
## Launch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
../gradlew run --args="BOT_TOKEN"
|
|
||||||
```
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is one of the most easiest bot - it will just print information about itself
|
|
||||||
*/
|
|
||||||
suspend fun main(vararg args: String) {
|
|
||||||
val botToken = args.first()
|
|
||||||
|
|
||||||
val bot = telegramBot(botToken)
|
|
||||||
|
|
||||||
println(bot.getMe())
|
|
||||||
}
|
|
||||||
44
GiftsBot/README.md
Normal file
44
GiftsBot/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# GiftsBot
|
||||||
|
|
||||||
|
A bot that retrieves and displays all gifts received by a user, chat, or business account.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
On `/start`, the bot fetches gifts from multiple sources (user gifts, chat gifts, business account
|
||||||
|
gifts) and sends a formatted summary to the user. Each gift is described with its type (regular or
|
||||||
|
unique, standard or business-owned) and relevant metadata.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Sample | Description |
|
||||||
|
|----------|-------|--------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Fetch and display all gifts for the requesting user |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Retrieves user gifts via `getUserGifts`
|
||||||
|
- Retrieves chat gifts via `getChatGifts`
|
||||||
|
- Retrieves business account gifts via `getBusinessAccountGifts`
|
||||||
|
- Distinguishes between regular gifts and unique gifts
|
||||||
|
- Distinguishes between standard (user-owned) and business-owned gifts
|
||||||
|
- Paginates through the full gift list
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
GiftsBot/build.gradle
Normal file
21
GiftsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="GiftsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
112
GiftsBot/src/main/kotlin/GiftsBot.kt
Normal file
112
GiftsBot/src/main/kotlin/GiftsBot.kt
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.gifts.getChatGiftsFlow
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.gifts.getUserGiftsFlow
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayContent
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCreated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayWinners
|
||||||
|
import dev.inmo.tgbotapi.types.chat.BusinessChat
|
||||||
|
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
||||||
|
import dev.inmo.tgbotapi.types.chat.PublicChat
|
||||||
|
import dev.inmo.tgbotapi.types.chat.UnknownChatType
|
||||||
|
import dev.inmo.tgbotapi.types.gifts.OwnedGift
|
||||||
|
import dev.inmo.tgbotapi.types.message.textsources.splitForText
|
||||||
|
import dev.inmo.tgbotapi.utils.bold
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, testServer = isTestServer) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
onCommand("start") {
|
||||||
|
val giftsFlow = when (val chat = it.chat) {
|
||||||
|
is BusinessChat -> {
|
||||||
|
getBusinessAccountGiftsFlow(
|
||||||
|
chat.id.businessConnectionId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PrivateChat -> {
|
||||||
|
getUserGiftsFlow(it.chat.id)
|
||||||
|
}
|
||||||
|
is UnknownChatType,
|
||||||
|
is PublicChat -> {
|
||||||
|
getChatGiftsFlow(it.chat.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withTypingAction(it.chat) {
|
||||||
|
val texts = buildEntities {
|
||||||
|
giftsFlow.collect { ownedGifts ->
|
||||||
|
ownedGifts.gifts.forEach {
|
||||||
|
when (it) {
|
||||||
|
is OwnedGift.Regular.Common -> {
|
||||||
|
bold("Type") + ": Regular common\n"
|
||||||
|
bold("Id") + ": ${it.gift.id.string}\n"
|
||||||
|
bold("Text") + ": ${it.text ?: "(None)"}\n"
|
||||||
|
bold("Stars cost") + ": ${it.gift.starCount}\n"
|
||||||
|
}
|
||||||
|
is OwnedGift.Unique.Common -> {
|
||||||
|
bold("Type") + ": Unique common\n"
|
||||||
|
bold("Id") + ": ${it.gift.id ?.string ?: "(None)"}\n"
|
||||||
|
bold("Name") + ": ${it.gift.name.value}\n"
|
||||||
|
bold("Model") + ": ${it.gift.model.name}\n"
|
||||||
|
bold("Number") + ": ${it.gift.number}\n"
|
||||||
|
}
|
||||||
|
is OwnedGift.Regular.OwnedByBusinessAccount -> {
|
||||||
|
bold("Type") + ": Regular owned by business\n"
|
||||||
|
bold("Id") + ": ${it.gift.id.string}\n"
|
||||||
|
bold("Text") + ": ${it.text ?: "(None)"}\n"
|
||||||
|
bold("Stars cost") + ": ${it.gift.starCount}\n"
|
||||||
|
}
|
||||||
|
is OwnedGift.Unique.OwnedByBusinessAccount -> {
|
||||||
|
bold("Type") + ": Unique owned by business\n"
|
||||||
|
bold("Id") + ": ${it.gift.id ?.string ?: "(None)"}\n"
|
||||||
|
bold("Name") + ": ${it.gift.name.value}\n"
|
||||||
|
bold("Model") + ": ${it.gift.model.name}\n"
|
||||||
|
bold("Number") + ": ${it.gift.number}\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val preparedTexts = texts.splitForText()
|
||||||
|
if (preparedTexts.isEmpty()) {
|
||||||
|
reply(it, "This chat have no any gifts")
|
||||||
|
} else {
|
||||||
|
preparedTexts.forEach { preparedText -> reply(it, preparedText) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
// println(it)
|
||||||
|
// }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
40
GiveawaysBot/README.md
Normal file
40
GiveawaysBot/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# GiveawaysBot
|
||||||
|
|
||||||
|
A bot that monitors and logs all giveaway lifecycle events in chats.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Listens for Telegram giveaway service messages and logs each event to standard output. No
|
||||||
|
interactive commands are provided; the bot is purely an observer/logger for giveaway activity.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Detects and logs giveaway creation events (`GiveawayCreated`)
|
||||||
|
- Detects and logs giveaway completion events with results (`Giveaway` with results)
|
||||||
|
- Detects and logs winner announcement messages (`GiveawayWinners`)
|
||||||
|
- Detects content messages that contain giveaway information (`GiveawayPublicResults`)
|
||||||
|
- All events are printed to stdout for inspection
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
GiveawaysBot/build.gradle
Normal file
21
GiveawaysBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="GiveawaysBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
57
GiveawaysBot/src/main/kotlin/GiveawaysBot.kt
Normal file
57
GiveawaysBot/src/main/kotlin/GiveawaysBot.kt
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayContent
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCreated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayWinners
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This place can be the playground for your code.
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, testServer = isTestServer) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
onGiveawayCreated {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onGiveawayCompleted {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onGiveawayWinners {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onGiveawayContent {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
// println(it)
|
||||||
|
// }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -1,6 +1,36 @@
|
|||||||
# HelloBot
|
# HelloBot
|
||||||
|
|
||||||
The main purpose of this bot is just to answer "Oh, hi, " and add user mention here
|
A minimal bot that responds whenever someone mentions the bot's username in a chat.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Listens for any message that contains the bot's username mention. When triggered, replies with
|
||||||
|
`Oh, hi, ` followed by a mention of the sender (or the group/channel name for non-private chats).
|
||||||
|
Uses MarkdownV2 formatting and adapts the reply text based on the chat type.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None. The bot is triggered by username mentions, not commands.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Detects mentions of the bot username in all chat types (private, group, supergroup, channel, business)
|
||||||
|
- Builds a MarkdownV2-formatted reply that links back to the sender
|
||||||
|
- For public chats the reply contains a clickable mention link; for private chats it uses a text mention
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.*
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMentionWithAnyContent
|
||||||
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.sender_chat
|
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.sender_chat
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
|
||||||
import dev.inmo.tgbotapi.extensions.utils.formatting.linkMarkdownV2
|
import dev.inmo.tgbotapi.extensions.utils.formatting.linkMarkdownV2
|
||||||
import dev.inmo.tgbotapi.extensions.utils.formatting.textMentionMarkdownV2
|
import dev.inmo.tgbotapi.extensions.utils.formatting.textMentionMarkdownV2
|
||||||
import dev.inmo.tgbotapi.extensions.utils.ifChannelChat
|
|
||||||
import dev.inmo.tgbotapi.extensions.utils.ifFromChannelGroupContentMessage
|
import dev.inmo.tgbotapi.extensions.utils.ifFromChannelGroupContentMessage
|
||||||
import dev.inmo.tgbotapi.types.chat.*
|
import dev.inmo.tgbotapi.types.chat.*
|
||||||
import dev.inmo.tgbotapi.types.chat.GroupChat
|
|
||||||
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
|
||||||
import dev.inmo.tgbotapi.types.chat.SupergroupChat
|
|
||||||
import dev.inmo.tgbotapi.types.message.MarkdownV2
|
import dev.inmo.tgbotapi.types.message.MarkdownV2
|
||||||
import dev.inmo.tgbotapi.utils.PreviewFeature
|
import dev.inmo.tgbotapi.utils.PreviewFeature
|
||||||
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
|
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main purpose of this bot is just to answer "Oh, hi, " and add user mention here
|
* The main purpose of this bot is just to answer "Oh, hi, " and add user mention here
|
||||||
@@ -25,21 +25,33 @@ suspend fun main(vararg args: String) {
|
|||||||
val botToken = args.first()
|
val botToken = args.first()
|
||||||
|
|
||||||
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
onContentMessage { message ->
|
val me = getMe()
|
||||||
val chat = message.chat
|
onContentMessage(
|
||||||
|
initialFilter = initialFilter@{ it.text ?.contains(me.username ?.full ?: return@initialFilter false) == true }
|
||||||
|
) { message ->
|
||||||
val answerText = when (val chat = message.chat) {
|
val answerText = when (val chat = message.chat) {
|
||||||
is ChannelChat -> {
|
is PreviewChannelChat -> {
|
||||||
val answer = "Hi everybody in this channel \"${chat.title}\""
|
val sender = message.sender_chat
|
||||||
reply(message, answer, MarkdownV2)
|
val answer = "Hi everybody in this channel \"${chat.title}\"" + if (sender != null) {
|
||||||
|
" and you, " + when (sender) {
|
||||||
|
is BusinessChat -> "business chat (wat) ${sender.original}"
|
||||||
|
is PrivateChat -> "${sender.lastName} ${sender.firstName}"
|
||||||
|
is GroupChat -> "group ${sender.title}"
|
||||||
|
is ChannelChat -> "channel ${sender.title}"
|
||||||
|
is UnknownChatType -> "wat chat (${sender})"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
reply(message, answer.escapeMarkdownV2Common(), MarkdownV2)
|
||||||
return@onContentMessage
|
return@onContentMessage
|
||||||
}
|
}
|
||||||
is PrivateChat -> {
|
is PreviewPrivateChat -> {
|
||||||
reply(message, "Hi, " + "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id), MarkdownV2)
|
reply(message, "Hi, " + "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id), MarkdownV2)
|
||||||
return@onContentMessage
|
return@onContentMessage
|
||||||
}
|
}
|
||||||
is GroupChat -> {
|
is PreviewGroupChat -> {
|
||||||
message.ifFromChannelGroupContentMessage {
|
message.ifFromChannelGroupContentMessage<Unit> {
|
||||||
val answer = "Hi, ${it.senderChat.title}"
|
val answer = "Hi, ${it.senderChat.title}"
|
||||||
reply(message, answer, MarkdownV2)
|
reply(message, answer, MarkdownV2)
|
||||||
return@onContentMessage
|
return@onContentMessage
|
||||||
@@ -53,9 +65,11 @@ suspend fun main(vararg args: String) {
|
|||||||
} ?: chat.title
|
} ?: chat.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UnknownExtendedChat,
|
is PreviewBusinessChat -> {
|
||||||
|
reply(message, "Hi, " + "${chat.original.firstName} ${chat.original.lastName} (as business chat :) )".textMentionMarkdownV2(chat.original.id), MarkdownV2)
|
||||||
|
return@onContentMessage
|
||||||
|
}
|
||||||
is UnknownChatType -> "Unknown :(".escapeMarkdownV2Common()
|
is UnknownChatType -> "Unknown :(".escapeMarkdownV2Common()
|
||||||
else -> error("Something went wrong: unknown type of chat $chat")
|
|
||||||
}
|
}
|
||||||
reply(
|
reply(
|
||||||
message,
|
message,
|
||||||
|
|||||||
41
InlineQueriesBot/README.md
Normal file
41
InlineQueriesBot/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# InlineQueriesBot
|
||||||
|
|
||||||
|
A multiplatform bot that answers inline queries with paginated article results.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Responds to inline queries by returning a page of article results. Each result includes a
|
||||||
|
description and a deep-link button. Navigation between pages is handled via the query offset
|
||||||
|
(next/previous buttons encoded in the result set).
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None. The bot is driven by inline queries (type `@BotUsername` in any chat).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Answers inline queries with `InlineQueryResultArticle` items
|
||||||
|
- Offset-based pagination: each result page encodes the next-page offset in the answer
|
||||||
|
- Each result includes a deep-link `InlineKeyboardButton` back to the bot
|
||||||
|
- Multiplatform module with a shared `commonMain` implementation and a JVM launcher entry point
|
||||||
|
- Requires *Inline Mode* to be enabled in BotFather settings
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
37
InlineQueriesBot/build.gradle
Normal file
37
InlineQueriesBot/build.gradle
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$nativePartTemplate"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
binaries {
|
||||||
|
executable {
|
||||||
|
mainClass.set("InlineQueriesBotKt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('stdlib')
|
||||||
|
|
||||||
|
api "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
api "io.ktor:ktor-client-logging:$ktor_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
68
InlineQueriesBot/src/commonMain/kotlin/Bot.kt
Normal file
68
InlineQueriesBot/src/commonMain/kotlin/Bot.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onDeepLink
|
||||||
|
import dev.inmo.tgbotapi.requests.answers.InlineQueryResultsButton
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueryId
|
||||||
|
import dev.inmo.tgbotapi.types.inlineQueryAnswerResultsLimit
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thi bot will create inline query answers. You
|
||||||
|
* should enable inline queries in bot settings
|
||||||
|
*/
|
||||||
|
suspend fun doInlineQueriesBot(token: String) {
|
||||||
|
val bot = telegramBot(token)
|
||||||
|
|
||||||
|
bot.buildBehaviourWithLongPolling(
|
||||||
|
defaultExceptionsHandler = { it.printStackTrace() },
|
||||||
|
) {
|
||||||
|
onBaseInlineQuery {
|
||||||
|
val page = it.offset.toIntOrNull() ?: 0
|
||||||
|
val results = (0 until inlineQueryAnswerResultsLimit.last).map {
|
||||||
|
(page * inlineQueryAnswerResultsLimit.last) + it
|
||||||
|
}
|
||||||
|
|
||||||
|
answer(
|
||||||
|
it,
|
||||||
|
results = results.map { resultNumber ->
|
||||||
|
val inlineQueryId = InlineQueryId(resultNumber.toString())
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
inlineQueryId,
|
||||||
|
"Title $resultNumber",
|
||||||
|
InputTextMessageContent(
|
||||||
|
buildEntities {
|
||||||
|
+"Result text of " + resultNumber.toString() + " result:\n"
|
||||||
|
+it.query
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description = "Description of $resultNumber result"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cachedTime = 0,
|
||||||
|
isPersonal = true,
|
||||||
|
button = InlineQueryResultsButton.Start(
|
||||||
|
"Text of button with page $page",
|
||||||
|
"deep_link_for_page_$page"
|
||||||
|
),
|
||||||
|
nextOffset = (page + 1).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeepLink { (message, deepLink) ->
|
||||||
|
reply(message, deepLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
println(getMe())
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
3
InlineQueriesBot/src/jvmMain/kotlin/InlineQueriesBot.kt
Normal file
3
InlineQueriesBot/src/jvmMain/kotlin/InlineQueriesBot.kt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
doInlineQueriesBot(args.first())
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runBlocking {
|
||||||
|
doInlineQueriesBot(args.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||||
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.*
|
import dev.inmo.tgbotapi.extensions.api.edit.text.editMessageText
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
|
||||||
import dev.inmo.tgbotapi.extensions.utils.withContent
|
import dev.inmo.tgbotapi.extensions.utils.withContent
|
||||||
import dev.inmo.tgbotapi.types.BotCommand
|
import dev.inmo.tgbotapi.types.BotCommand
|
||||||
|
import dev.inmo.tgbotapi.types.CustomEmojiId
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent
|
||||||
|
import dev.inmo.tgbotapi.types.InlineQueryId
|
||||||
|
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonStyle
|
||||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
import dev.inmo.tgbotapi.utils.*
|
import dev.inmo.tgbotapi.utils.PreviewFeature
|
||||||
import kotlinx.coroutines.*
|
import dev.inmo.tgbotapi.utils.botCommand
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
private const val nextPageData = "next"
|
import dev.inmo.tgbotapi.utils.row
|
||||||
private const val previousPageData = "previous"
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
|
||||||
fun String.parsePageAndCount(): Pair<Int, Int>? {
|
fun String.parsePageAndCount(): Pair<Int, Int>? {
|
||||||
val (pageString, countString) = split(" ").takeIf { it.count() > 1 } ?: return null
|
val (pageString, countString) = split(" ").takeIf { it.count() > 1 } ?: return null
|
||||||
@@ -39,24 +47,38 @@ fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
row {
|
||||||
|
copyTextButton("Command copy button", "/inline $page $count")
|
||||||
|
}
|
||||||
|
|
||||||
row {
|
row {
|
||||||
if (page - 1 > 2) {
|
if (page - 1 > 2) {
|
||||||
dataButton("<<", "1 $count")
|
dataButton("<<", "1 $count", style = KeyboardButtonStyle.Danger)
|
||||||
}
|
}
|
||||||
if (page - 1 > 1) {
|
if (page - 1 > 1) {
|
||||||
dataButton("<", "${page - 2} $count")
|
dataButton("<", "${page - 2} $count", style = KeyboardButtonStyle.Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page + 1 < count) {
|
if (page + 1 < count) {
|
||||||
dataButton(">", "${page + 2} $count")
|
dataButton(">", "${page + 2} $count", style = KeyboardButtonStyle.Success)
|
||||||
}
|
}
|
||||||
if (page + 2 < count) {
|
if (page + 2 < count) {
|
||||||
dataButton(">>", "$count $count")
|
dataButton(">>", "$count $count", style = KeyboardButtonStyle.Danger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
row {
|
||||||
|
inlineQueryInChosenChatButton(
|
||||||
|
"Send somebody page",
|
||||||
|
query = "$page $count",
|
||||||
|
allowUsers = true,
|
||||||
|
allowBots = true,
|
||||||
|
allowGroups = true,
|
||||||
|
allowChannels = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(PreviewFeature::class)
|
||||||
suspend fun activateKeyboardsBot(
|
suspend fun activateKeyboardsBot(
|
||||||
token: String,
|
token: String,
|
||||||
print: (Any) -> Unit
|
print: (Any) -> Unit
|
||||||
@@ -67,13 +89,13 @@ suspend fun activateKeyboardsBot(
|
|||||||
|
|
||||||
bot.buildBehaviourWithLongPolling(CoroutineScope(currentCoroutineContext() + SupervisorJob())) {
|
bot.buildBehaviourWithLongPolling(CoroutineScope(currentCoroutineContext() + SupervisorJob())) {
|
||||||
onCommandWithArgs("inline") { message, args ->
|
onCommandWithArgs("inline") { message, args ->
|
||||||
val numberOfPages = args.firstOrNull() ?.toIntOrNull() ?: 10
|
val numberArgs = args.mapNotNull { it.toIntOrNull() }
|
||||||
|
val numberOfPages = numberArgs.getOrNull(1) ?: numberArgs.firstOrNull() ?: 10
|
||||||
|
val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1
|
||||||
reply(
|
reply(
|
||||||
message,
|
message,
|
||||||
replyMarkup = inlineKeyboard {
|
replyMarkup = inlineKeyboard {
|
||||||
row {
|
includePageButtons(page, numberOfPages)
|
||||||
includePageButtons(1, numberOfPages)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
regular("Your inline keyboard with $numberOfPages pages")
|
regular("Your inline keyboard with $numberOfPages pages")
|
||||||
@@ -92,9 +114,23 @@ suspend fun activateKeyboardsBot(
|
|||||||
return@onMessageDataCallbackQuery
|
return@onMessageDataCallbackQuery
|
||||||
},
|
},
|
||||||
replyMarkup = inlineKeyboard {
|
replyMarkup = inlineKeyboard {
|
||||||
row {
|
|
||||||
includePageButtons(page, count)
|
includePageButtons(page, count)
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
regular("This is $page of $count")
|
||||||
|
}
|
||||||
|
answer(it)
|
||||||
|
}
|
||||||
|
onInlineMessageIdDataCallbackQuery {
|
||||||
|
val (page, count) = it.data.parsePageAndCount() ?: it.let {
|
||||||
|
answer(it, "Unsupported data :(")
|
||||||
|
return@onInlineMessageIdDataCallbackQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
editMessageText(
|
||||||
|
it.inlineMessageId,
|
||||||
|
replyMarkup = inlineKeyboard {
|
||||||
|
includePageButtons(page, count)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
regular("This is $page of $count")
|
regular("This is $page of $count")
|
||||||
@@ -102,12 +138,32 @@ suspend fun activateKeyboardsBot(
|
|||||||
answer(it)
|
answer(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBaseInlineQuery {
|
||||||
|
val page = it.query.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery
|
||||||
|
val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() }
|
||||||
|
.toIntOrNull() ?: return@onBaseInlineQuery
|
||||||
|
|
||||||
|
answer(
|
||||||
|
it,
|
||||||
|
results = listOf(
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
InlineQueryId(it.query),
|
||||||
|
"Send buttons",
|
||||||
|
InputTextMessageContent("It is sent via inline mode inline buttons"),
|
||||||
|
replyMarkup = inlineKeyboard {
|
||||||
|
includePageButtons(page, count)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onUnhandledCommand {
|
onUnhandledCommand {
|
||||||
reply(
|
reply(
|
||||||
it,
|
it,
|
||||||
replyMarkup = replyKeyboard(resizeKeyboard = true, oneTimeKeyboard = true) {
|
replyMarkup = replyKeyboard(resizeKeyboard = true, oneTimeKeyboard = true) {
|
||||||
row {
|
row {
|
||||||
simpleButton("/inline")
|
simpleButton("/inline", style = KeyboardButtonStyle.Primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -117,7 +173,7 @@ suspend fun activateKeyboardsBot(
|
|||||||
|
|
||||||
setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard"))
|
setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard"))
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
}.join()
|
}.join()
|
||||||
|
|||||||
47
KeyboardsBot/README.md
Normal file
47
KeyboardsBot/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# KeyboardsBot
|
||||||
|
|
||||||
|
A multiplatform bot (JVM + JS) that demonstrates inline keyboard pagination and various button types.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
On `/inline <page> <count>`, the bot sends an inline keyboard built from `count` items starting
|
||||||
|
at `page`. The keyboard includes previous/next navigation buttons, copy-text buttons, styled
|
||||||
|
action buttons, and an inline-query chosen-chat button. Callback queries from the buttons navigate
|
||||||
|
between pages.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional flags (any order):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Arguments | Description |
|
||||||
|
|---------|-----------|-------------|
|
||||||
|
| `/inline` | `<page> <count>` | Send a paginated inline keyboard starting at `page` with `count` items per page |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Multi-page inline keyboard navigation (previous / next buttons encoded as callback data)
|
||||||
|
- Copy-text buttons (`CopyTextButton`)
|
||||||
|
- Styled action buttons: Primary, Success, Danger colour variants
|
||||||
|
- Inline query chosen-chat button (`SwitchInlineQueryChosenChat`)
|
||||||
|
- Answers inline queries that originate from the keyboard buttons
|
||||||
|
- Shared `commonMain` library with JVM and JS launchers
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :KeyboardsBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun main(args: Array<String>) {
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) { // IO for inheriting of it in side of activateKeyboardsBot
|
withContext(Dispatchers.IO) { // IO for inheriting of it in side of activateKeyboardsBot
|
||||||
activateKeyboardsBot(args.first()) {
|
activateKeyboardsBot(args.first()) {
|
||||||
println(it)
|
println(it)
|
||||||
|
|||||||
46
LinkPreviewsBot/README.md
Normal file
46
LinkPreviewsBot/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# LinkPreviewsBot
|
||||||
|
|
||||||
|
A bot that demonstrates all `LinkPreviewOptions` variants by replying with multiple messages, each
|
||||||
|
using a different link preview style.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
When the user sends a message containing a URL, the bot extracts the URL and sends several reply
|
||||||
|
messages, each with a different `LinkPreviewOptions` configuration: disabled, small preview above
|
||||||
|
text, large preview above text, small preview below text, large preview below text, and the default
|
||||||
|
(no explicit options).
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Extracts URLs from the text entities of incoming messages
|
||||||
|
- Sends one reply per `LinkPreviewOptions` variant:
|
||||||
|
- Preview disabled
|
||||||
|
- Small image, positioned above text
|
||||||
|
- Large image, positioned above text
|
||||||
|
- Small image, positioned below text
|
||||||
|
- Large image, positioned below text
|
||||||
|
- Default (Telegram-chosen behaviour)
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
LinkPreviewsBot/build.gradle
Normal file
21
LinkPreviewsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="LinkPreviewsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
84
LinkPreviewsBot/src/main/kotlin/LinkPreviewsBot.kt
Normal file
84
LinkPreviewsBot/src/main/kotlin/LinkPreviewsBot.kt
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.textLinkTextSourceOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.uRLTextSourceOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.LinkPreviewOptions
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.TextedContent
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bot will reply with the same
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.getOrNull(1) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bot = telegramBot(botToken)
|
||||||
|
|
||||||
|
bot.buildBehaviourWithLongPolling {
|
||||||
|
onContentMessage { contentMessage ->
|
||||||
|
val url = contentMessage.withContentOrNull<TextedContent>() ?.let { message ->
|
||||||
|
message.content.textSources.firstNotNullOfOrNull {
|
||||||
|
it.textLinkTextSourceOrNull() ?.url ?: it.uRLTextSourceOrNull() ?.source
|
||||||
|
}
|
||||||
|
} ?: null.apply {
|
||||||
|
reply(contentMessage) {
|
||||||
|
regular("I am support only content with text contains url only")
|
||||||
|
}
|
||||||
|
} ?: return@onContentMessage
|
||||||
|
contentMessage.withContentOrNull<TextedContent>() ?.let {
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Disabled
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Large(url, showAboveText = true)
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Large(url, showAboveText = false)
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Small(url, showAboveText = true)
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Small(url, showAboveText = false)
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Default(url, showAboveText = true)
|
||||||
|
)
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
it.content.textSources,
|
||||||
|
linkPreviewOptions = LinkPreviewOptions.Default(url, showAboveText = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
42
LiveLocationsBot/README.md
Normal file
42
LiveLocationsBot/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# LiveLocationsBot
|
||||||
|
|
||||||
|
A bot that sends a live location and updates it periodically until the user cancels.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
On `/start`, the bot sends a live location message with an inline *Cancel* button. A coroutine then
|
||||||
|
updates the location every 3 seconds with a slightly changing coordinate. When the user presses
|
||||||
|
*Cancel* the update loop is stopped and the live location is closed.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Sample | Description |
|
||||||
|
|----------|-------|--------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Sample | Description |
|
||||||
|
|-------|--------|-------------|
|
||||||
|
| `debug` | `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Sends a live location message and starts the position update loop |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Sends an initial live location using `sendLiveLocation`
|
||||||
|
- Updates the location every 3 seconds via `editLiveLocation` in a background coroutine
|
||||||
|
- Inline keyboard with a *Cancel* callback button
|
||||||
|
- Handles the cancel callback to stop the update loop and close the live location
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
LiveLocationsBot/build.gradle
Normal file
21
LiveLocationsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="LiveLocationsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
67
LiveLocationsBot/src/main/kotlin/LiveLocationsBot.kt
Normal file
67
LiveLocationsBot/src/main/kotlin/LiveLocationsBot.kt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.EditLiveLocationInfo
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.edit.location.live.stopLiveLocation
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.handleLiveLocation
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.LocationContent
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bot will send you live location and update it from time to time
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
|
val locationsFlow = flow {
|
||||||
|
var i = 0
|
||||||
|
while (isActive) {
|
||||||
|
val newInfo = EditLiveLocationInfo(
|
||||||
|
latitude = i.toDouble(),
|
||||||
|
longitude = i.toDouble(),
|
||||||
|
replyMarkup = flatInlineKeyboard {
|
||||||
|
dataButton("Cancel", "cancel")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
emit(newInfo)
|
||||||
|
i++
|
||||||
|
delay(3000L) // 3 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("start") {
|
||||||
|
// in this flow will be actual message with live location
|
||||||
|
val currentMessageState = MutableStateFlow<ContentMessage<LocationContent>?>(null)
|
||||||
|
val sendingJob = launch {
|
||||||
|
handleLiveLocation(
|
||||||
|
it.chat.id,
|
||||||
|
locationsFlow,
|
||||||
|
sentMessageFlow = { currentMessageState.emit(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitMessageDataCallbackQuery().filter {
|
||||||
|
it.message.sameMessage(
|
||||||
|
currentMessageState.value ?: return@filter false
|
||||||
|
) && it.data == "cancel"
|
||||||
|
}.first()
|
||||||
|
|
||||||
|
sendingJob.cancel() // ends live location
|
||||||
|
currentMessageState.value ?.let {
|
||||||
|
stopLiveLocation(it, replyMarkup = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
|
|
||||||
47
ManagedBotsBot/README.md
Normal file
47
ManagedBotsBot/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# ManagedBotsBot
|
||||||
|
|
||||||
|
A bot that demonstrates the Managed Bots API: creating child bots and replacing their tokens.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Allows the operator to check whether the bot supports managed bots, create new managed bots via a
|
||||||
|
keyboard button, and replace an existing managed bot's token. When a managed bot is created its
|
||||||
|
token is sent back to the operator. The bot also demonstrates custom middleware and subcontext usage.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/canManageBots` | Check whether this bot has the ability to create managed bots |
|
||||||
|
| `/keyboard` | Send a reply keyboard with a *Create managed bot* button |
|
||||||
|
| `/replaceToken` | Replace the token of a managed bot (send as reply to the bot's token message) |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Queries bot capabilities via `getMe` extended fields
|
||||||
|
- Creates a managed child bot via the `BotKeyboardButton` with `RequestBot` type
|
||||||
|
- Receives the new bot's info in a `BotShared` service message
|
||||||
|
- Replaces a managed bot's token via `replaceStickerInSet` (token replacement API)
|
||||||
|
- Handles `ManagedBotUpdated` events for tracking child bot status changes
|
||||||
|
- Custom request middleware for logging
|
||||||
|
- Custom `BehaviourContext` subcontext
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
ManagedBotsBot/build.gradle
Normal file
21
ManagedBotsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="CustomBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
143
ManagedBotsBot/src/main/kotlin/ManagedBotsBot.kt
Normal file
143
ManagedBotsBot/src/main/kotlin/ManagedBotsBot.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.managed_bots.getManagedBotToken
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.managed_bots.replaceManagedBotToken
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotCreated
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotUpdated
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.chatEventMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.groupContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.managedBotCreatedOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatReplyKeyboard
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.requestManagedBotButton
|
||||||
|
import dev.inmo.tgbotapi.types.Username
|
||||||
|
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestManagedBot
|
||||||
|
import dev.inmo.tgbotapi.types.buttons.PreparedKeyboardButtonId
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
|
import dev.inmo.tgbotapi.types.toChatId
|
||||||
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
private var BehaviourContextData.update: Update?
|
||||||
|
get() = get("update") as? Update
|
||||||
|
set(value) = set("update", value)
|
||||||
|
|
||||||
|
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
||||||
|
get() = get("commonMessage") as? CommonMessage<*>
|
||||||
|
set(value) = set("commonMessage", value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This place can be the playground for your code.
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.IO),
|
||||||
|
testServer = isTestServer,
|
||||||
|
builder = {
|
||||||
|
includeMiddlewares {
|
||||||
|
addMiddleware {
|
||||||
|
doOnRequestReturnResult { result, request, _ ->
|
||||||
|
println("Result of $request:\n\n$result")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subcontextInitialAction = buildSubcontextInitialAction {
|
||||||
|
add {
|
||||||
|
data.update = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
onCommand("start") {
|
||||||
|
println(data.update)
|
||||||
|
println(data.commonMessage)
|
||||||
|
println(getChat(it.chat))
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("canManageBots") {
|
||||||
|
val me = getMe()
|
||||||
|
reply(it, if (me.canManageBots) "Yes" else "No")
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestId = RequestId(0)
|
||||||
|
onCommand("keyboard") {
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
"Keyboard",
|
||||||
|
replyMarkup = flatReplyKeyboard(
|
||||||
|
resizeKeyboard = true,
|
||||||
|
oneTimeKeyboard = true,
|
||||||
|
) {
|
||||||
|
requestManagedBotButton(
|
||||||
|
"Add managed bot",
|
||||||
|
KeyboardButtonRequestManagedBot(
|
||||||
|
requestId = requestId,
|
||||||
|
suggestedName = "SampleName",
|
||||||
|
suggestedUsername = Username("@some_sample_bot")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onManagedBotCreated {
|
||||||
|
reply(it, "Managed bot created successfully: ${it.chatEvent.bot}")
|
||||||
|
val token = getManagedBotToken(
|
||||||
|
it.chatEvent.bot.id.toChatId()
|
||||||
|
)
|
||||||
|
reply(it, "Token: $token")
|
||||||
|
}
|
||||||
|
|
||||||
|
onManagedBotUpdated {
|
||||||
|
send(it.user, "Managed bot has been updated: ${it.bot}")
|
||||||
|
val token = getManagedBotToken(
|
||||||
|
it.bot.id.toChatId()
|
||||||
|
)
|
||||||
|
send(it.user, "Token: $token")
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("replaceToken") {
|
||||||
|
val reply = it.replyTo ?.chatEventMessageOrNull() ?: return@onCommand
|
||||||
|
val managedBotCreated = reply.chatEvent.managedBotCreatedOrNull() ?: return@onCommand
|
||||||
|
|
||||||
|
reply(it, "Token in replace update: ${replaceManagedBotToken(managedBotCreated.bot.id.toChatId())}")
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
42
MemberUpdatedWatcherBot/README.md
Normal file
42
MemberUpdatedWatcherBot/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# MemberUpdatedWatcherBot
|
||||||
|
|
||||||
|
A bot that monitors all `ChatMemberUpdated` events and sends descriptive notifications to the chat.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Watches for every member status change in all chats the bot is a member of: bot additions,
|
||||||
|
admin promotions and demotions, user joins and leaves, and permission restriction changes.
|
||||||
|
For each event the bot sends a human-readable message describing what changed.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Detects when the bot itself is added to or removed from a chat and sends a greeting/farewell
|
||||||
|
- Detects when the bot is promoted to or demoted from administrator
|
||||||
|
- Detects when any user joins or leaves the chat
|
||||||
|
- Detects when any user is promoted to or demoted from administrator
|
||||||
|
- Detects granular permission changes (e.g., restrictions added or lifted)
|
||||||
|
- Uses the `ChatMemberUpdated` extension functions introduced in TelegramBotAPI 18.0.0
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
MemberUpdatedWatcherBot/build.gradle
Normal file
21
MemberUpdatedWatcherBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="MemberUpdatedWatcherKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import dev.inmo.kslog.common.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictedFilter
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictionsChangedFilter
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.*
|
||||||
|
import dev.inmo.tgbotapi.types.chat.member.*
|
||||||
|
import dev.inmo.tgbotapi.utils.*
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(PreviewFeature::class)
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
val token = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalLogger = KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag ?: "ChatMemberUpdates", message, throwable))
|
||||||
|
}
|
||||||
|
|
||||||
|
val bot = telegramBot(token)
|
||||||
|
|
||||||
|
bot.buildBehaviourWithLongPolling {
|
||||||
|
val me = getMe()
|
||||||
|
val filterSelfUpdates = SimpleFilter<ChatMemberUpdated> {
|
||||||
|
it.member.id == me.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// This bot updates
|
||||||
|
onChatMemberJoined(initialFilter = filterSelfUpdates) {
|
||||||
|
internalLogger.i("Bot was added to chat")
|
||||||
|
send(it.chat.id, "I was added to chat. Please grant me admin permissions to make me able to watch other users' events")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberGotPromoted(initialFilter = filterSelfUpdates) {
|
||||||
|
internalLogger.i("Bot was granted admin permissions")
|
||||||
|
send(it.chat.id, "I was promoted to admin. I now can watch other users' events")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberGotDemoted(initialFilter = filterSelfUpdates) {
|
||||||
|
internalLogger.i("Admin permissions were revoked")
|
||||||
|
send(it.chat.id, "I'm no longer an admin. Admin permissions are required to watch other users' events")
|
||||||
|
}
|
||||||
|
|
||||||
|
// All users updates
|
||||||
|
onChatMemberJoined {
|
||||||
|
val member = it.member
|
||||||
|
internalLogger.i("${member.firstName} joined the chat: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
|
||||||
|
send(it.chat.id, "Welcome ${member.firstName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberLeft {
|
||||||
|
val member = it.member
|
||||||
|
internalLogger.i("${member.firstName} left the chat: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
|
||||||
|
send(it.chat.id, "Goodbye ${member.firstName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberGotPromoted {
|
||||||
|
val newState = it.newChatMemberState.administratorChatMemberOrThrow()
|
||||||
|
internalLogger.i("${newState.user.firstName} got promoted to ${newState.customTitle ?: "Admin"}: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
|
||||||
|
send(it.chat.id, "${newState.user.firstName} is now an ${newState.customTitle ?: "Admin"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberGotDemoted {
|
||||||
|
val member = it.member
|
||||||
|
internalLogger.i("${member.firstName} got demoted: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
|
||||||
|
send(it.chat.id, "${member.firstName} is now got demoted back to member")
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberGotPromotionChanged {
|
||||||
|
val member = it.member
|
||||||
|
val message = "${member.firstName} has the permissions changed: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}"
|
||||||
|
internalLogger.i(message)
|
||||||
|
send(it.chat.id, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatMemberUpdated(
|
||||||
|
initialFilter = chatMemberGotRestrictedFilter + chatMemberGotRestrictionsChangedFilter,
|
||||||
|
) {
|
||||||
|
val member = it.member
|
||||||
|
val message = "${member.firstName} has the permissions changed: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}"
|
||||||
|
internalLogger.i(message)
|
||||||
|
send(it.chat.id, message)
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
39
MyBot/README.md
Normal file
39
MyBot/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# MyBot
|
||||||
|
|
||||||
|
A bot that monitors messages containing its username and responds with a contextual link or mention.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Watches every incoming message for a mention of the bot's username. When found, it replies with a
|
||||||
|
MarkdownV2-formatted message that includes a link or text mention pointing back to the originating
|
||||||
|
chat or user, adapting the reply to the type of chat the message came from.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None. The bot reacts to username mentions, not to commands.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Handles all chat types: private, group, supergroup, channel, business connection chats, channel groups
|
||||||
|
- For public chats builds a `t.me/<username>` hyperlink; for private chats uses an inline text mention
|
||||||
|
- Prints information about the originating chat using `getChat`
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
91
MyBot/src/main/kotlin/MyBot.kt
Normal file
91
MyBot/src/main/kotlin/MyBot.kt
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.removeMyProfilePhoto
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyProfilePhoto
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitPhotoMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||||
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
|
import dev.inmo.tgbotapi.requests.business_connection.InputProfilePhoto
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is one of the easiest bots - it will just print information about itself
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bot = telegramBot(botToken)
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.Default),
|
||||||
|
testServer = isTestServer,
|
||||||
|
) {
|
||||||
|
val me = bot.getMe()
|
||||||
|
println(me)
|
||||||
|
println(bot.getChat(me))
|
||||||
|
|
||||||
|
onCommand("setMyProfilePhoto") { commandMessage ->
|
||||||
|
reply(commandMessage, "ok, send me new photo")
|
||||||
|
val newPhotoMessage = waitPhotoMessage().filter { potentialPhotoMessage ->
|
||||||
|
potentialPhotoMessage.sameChat(commandMessage)
|
||||||
|
}.first()
|
||||||
|
val draftMessagesChannel = Channel<String>(capacity = 1)
|
||||||
|
|
||||||
|
launchLoggingDropExceptions {
|
||||||
|
sendMessageDraftFlowWithTexts(commandMessage.chat.id, draftMessagesChannel.consumeAsFlow())
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
draftMessagesChannel.close(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
draftMessagesChannel.send("Start downloading photo")
|
||||||
|
val photoFile = downloadFileToTemp(newPhotoMessage.content)
|
||||||
|
|
||||||
|
draftMessagesChannel.send("Photo file have been downloaded. Start set my profile photo")
|
||||||
|
|
||||||
|
setMyProfilePhoto(
|
||||||
|
InputProfilePhoto.Static(
|
||||||
|
photoFile.asMultipartFile()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
reply(commandMessage, "New photo have been set")
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("removeMyProfilePhoto") {
|
||||||
|
runCatchingLogging {
|
||||||
|
removeMyProfilePhoto()
|
||||||
|
reply(it, "Photo have been removed")
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTrace()
|
||||||
|
reply(it, "Something web wrong. See logs for details.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
48
PollsBot/README.md
Normal file
48
PollsBot/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# PollsBot
|
||||||
|
|
||||||
|
A bot that demonstrates creation and management of Telegram polls (anonymous, public, and quiz).
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Creates polls on demand and tracks live answer updates. Users can reply to an existing poll message
|
||||||
|
to add new options or remove the last option. Quiz polls are created with a random correct answer.
|
||||||
|
Custom emoji stickers in poll options are supported.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/anonymous` | Create an anonymous poll |
|
||||||
|
| `/public` | Create a public poll; users can add options by replying |
|
||||||
|
| `/quiz` | Create a quiz poll with a randomly chosen correct answer |
|
||||||
|
|
||||||
|
All three commands accept an optional custom emoji ID as an extra argument to use in poll option text.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Mutex-protected in-memory poll registry to safely track concurrent updates
|
||||||
|
- Live poll answer updates via `onPollUpdated` handler
|
||||||
|
- Reply-based option management: reply to a poll message with text to add an option, or reply with `/remove` to delete the last option
|
||||||
|
- Quiz polls: random correct answer selection, answer explanation included
|
||||||
|
- Custom emoji in poll option text
|
||||||
|
- Registers all three commands with Telegram (`setMyCommands`)
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
PollsBot/build.gradle
Normal file
21
PollsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="HelloBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
228
PollsBot/src/main/kotlin/PollsBot.kt
Normal file
228
PollsBot/src/main/kotlin/PollsBot.kt
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.polls.sendQuizPoll
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.polls.sendRegularPoll
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollAnswer
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionAdded
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionDeleted
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollUpdates
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.accessibleMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.customEmojiTextSourceOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgsSources
|
||||||
|
import dev.inmo.tgbotapi.types.BotCommand
|
||||||
|
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||||
|
import dev.inmo.tgbotapi.types.PollId
|
||||||
|
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||||
|
import dev.inmo.tgbotapi.types.polls.InputPollOption
|
||||||
|
import dev.inmo.tgbotapi.types.polls.PollAnswer
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
|
import dev.inmo.tgbotapi.utils.customEmoji
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
|
import dev.inmo.tgbotapi.utils.underline
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bot will answer with anonymous or public poll and send message on
|
||||||
|
* any update.
|
||||||
|
*
|
||||||
|
* * Use `/anonymous` to take anonymous regular poll
|
||||||
|
* * Use `/public` to take public regular poll
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
|
||||||
|
val pollToChat = mutableMapOf<PollId, IdChatIdentifier>()
|
||||||
|
val pollToChatMutex = Mutex()
|
||||||
|
|
||||||
|
onCommand("anonymous", requireOnlyCommandInMessage = false) {
|
||||||
|
val customEmoji = it.content.parseCommandsWithArgsSources()
|
||||||
|
.toList()
|
||||||
|
.firstOrNull { it.first.command == "anonymous" }
|
||||||
|
?.second
|
||||||
|
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
|
||||||
|
val sentPoll = sendRegularPoll(
|
||||||
|
it.chat.id,
|
||||||
|
buildEntities {
|
||||||
|
regular("Test regular anonymous poll")
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(1 .. 10).map {
|
||||||
|
InputPollOption {
|
||||||
|
regular(it.toString()) + " "
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isAnonymous = true,
|
||||||
|
replyParameters = ReplyParameters(it)
|
||||||
|
)
|
||||||
|
pollToChatMutex.withLock {
|
||||||
|
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("public", requireOnlyCommandInMessage = false) {
|
||||||
|
val customEmoji = it.content.parseCommandsWithArgsSources()
|
||||||
|
.toList()
|
||||||
|
.firstOrNull { it.first.command == "public" }
|
||||||
|
?.second
|
||||||
|
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
|
||||||
|
val sentPoll = sendRegularPoll(
|
||||||
|
it.chat.id,
|
||||||
|
buildEntities {
|
||||||
|
regular("Test regular non anonymous poll")
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(1 .. 10).map {
|
||||||
|
InputPollOption {
|
||||||
|
regular(it.toString()) + " "
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isAnonymous = false,
|
||||||
|
replyParameters = ReplyParameters(it),
|
||||||
|
allowAddingOptions = true,
|
||||||
|
hideResultsUntilCloses = true,
|
||||||
|
)
|
||||||
|
pollToChatMutex.withLock {
|
||||||
|
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("quiz", requireOnlyCommandInMessage = false) {
|
||||||
|
val customEmoji = it.content.parseCommandsWithArgsSources()
|
||||||
|
.toList()
|
||||||
|
.firstOrNull { it.first.command == "quiz" }
|
||||||
|
?.second
|
||||||
|
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
|
||||||
|
val correctAnswer = mutableListOf<Int>()
|
||||||
|
(1 until Random.nextInt(9)).forEach {
|
||||||
|
val option = Random.nextInt(10)
|
||||||
|
if (correctAnswer.contains(option)) return@forEach
|
||||||
|
correctAnswer.add(option)
|
||||||
|
}
|
||||||
|
val sentPoll = sendQuizPoll(
|
||||||
|
it.chat.id,
|
||||||
|
questionEntities = buildEntities {
|
||||||
|
regular("Test quiz poll")
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
descriptionTextSources = buildEntities {
|
||||||
|
regular("Test quiz poll description:")
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options = (1 .. 10).map {
|
||||||
|
InputPollOption {
|
||||||
|
regular(it.toString()) + " "
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isAnonymous = false,
|
||||||
|
replyParameters = ReplyParameters(it),
|
||||||
|
correctOptionIds = correctAnswer.sorted(),
|
||||||
|
allowsMultipleAnswers = correctAnswer.size > 1,
|
||||||
|
allowsRevoting = true,
|
||||||
|
shuffleOptions = true,
|
||||||
|
hideResultsUntilCloses = true,
|
||||||
|
explanationTextSources = buildEntities {
|
||||||
|
regular("Random solved it to be ") + underline((correctAnswer + 1).toString()) + " "
|
||||||
|
if (customEmoji != null) {
|
||||||
|
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
println("Sent poll data: $sentPoll")
|
||||||
|
pollToChatMutex.withLock {
|
||||||
|
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPollAnswer {
|
||||||
|
val chatId = pollToChat[it.pollId] ?: return@onPollAnswer
|
||||||
|
|
||||||
|
when(it) {
|
||||||
|
is PollAnswer.Public -> send(chatId, "[onPollAnswer] User ${it.user} have answered")
|
||||||
|
is PollAnswer.Anonymous -> send(chatId, "[onPollAnswer] Chat ${it.voterChat} have answered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPollUpdates {
|
||||||
|
val chatId = pollToChat[it.id] ?: return@onPollUpdates
|
||||||
|
|
||||||
|
when(it.isAnonymous) {
|
||||||
|
false -> send(chatId, "[onPollUpdates] Public poll updated: ${it.options.joinToString()}")
|
||||||
|
true -> send(chatId, "[onPollUpdates] Anonymous poll updated: ${it.options.joinToString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPollOptionAdded {
|
||||||
|
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
||||||
|
reply(pollMessage) {
|
||||||
|
+"Poll option added: \n"
|
||||||
|
+it.chatEvent.optionTextSources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPollOptionDeleted {
|
||||||
|
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
||||||
|
reply(pollMessage) {
|
||||||
|
+"Poll option deleted: \n"
|
||||||
|
+it.chatEvent.optionTextSources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentMessage {
|
||||||
|
val replyPollOptionId = it.replyInfo ?.pollOptionId ?: return@onContentMessage
|
||||||
|
it.replyTo ?.accessibleMessageOrNull() ?.let { replied ->
|
||||||
|
reply(replied, pollOptionId = replyPollOptionId) {
|
||||||
|
+"Reply to poll option"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMyCommands(
|
||||||
|
BotCommand("anonymous", "Create anonymous regular poll"),
|
||||||
|
BotCommand("public", "Create non anonymous regular poll"),
|
||||||
|
BotCommand("quiz", "Create quiz poll with random right answer"),
|
||||||
|
)
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ This repository contains several examples of simple bots which are using Telegra
|
|||||||
|
|
||||||
## How to use this repository
|
## How to use this repository
|
||||||
|
|
||||||
|
***TO RUN NATIVE TARGETS ON LINUX YOU SHOULD INSTALL CURL LIBRARY. FOR EXAMPLE: `sudo apt install libcurl4-gnutls-dev`***
|
||||||
|
|
||||||
This repository contains several important things:
|
This repository contains several important things:
|
||||||
|
|
||||||
* Example subprojects
|
* Example subprojects
|
||||||
|
|||||||
@@ -1,9 +1,53 @@
|
|||||||
# RandomFileSenderBot
|
# RandomFileSenderBot
|
||||||
|
|
||||||
This bot will send random file from input folder OR from bot working folder
|
A multiplatform bot (JVM + Native) that picks random files from a directory and sends them to the
|
||||||
|
requester.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Picks one or more files at random from a specified directory and sends them to the user. Multiple
|
||||||
|
files are batched into a media group. Files are sent as protected content. The bot is implemented
|
||||||
|
as a shared library with separate JVM and Native launcher entry points.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
| 2 *(optional)* | `/path/to/dir` | Directory to pick files from (defaults to the current working directory) |
|
||||||
|
|
||||||
|
Optional flags (any order):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/send_file` | Send 1 random file from the configured directory |
|
||||||
|
| `/send_file N` | Send *N* random files from the configured directory |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Platform-specific random file selection (JVM uses `java.io.File`, Native uses POSIX directory API)
|
||||||
|
- Groups multiple files into a single media group message when N > 1
|
||||||
|
- Files are sent as protected content (forwarding disabled)
|
||||||
|
- Multiplatform: shared logic in `commonMain`, launchers in `jvm_launcher` and `native_launcher`
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
../gradlew run --args="BOT_TOKEN[ optional/folder/path]"
|
./gradlew :RandomFileSenderBot:jvm_launcher:run --args="BOT_TOKEN /optional/path"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native (after build)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./RandomFileSenderBot/native_launcher/build/bin/native/releaseExecutable/native_launcher.kexe BOT_TOKEN /optional/path
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,14 +8,29 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
plugins {
|
||||||
apply plugin: 'application'
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
|
||||||
mainClassName="RandomFileSenderBotKt"
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
|
|
||||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
binaries {
|
||||||
|
executable {
|
||||||
|
mainClass.set("RandomFileSenderBotKt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('stdlib')
|
||||||
|
|
||||||
|
api "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$nativePartTemplate"
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
import dev.inmo.micro_utils.common.filesize
|
import dev.inmo.micro_utils.common.filesize
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
|
||||||
import dev.inmo.tgbotapi.bot.TelegramBot
|
import dev.inmo.tgbotapi.bot.TelegramBot
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.*
|
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.media.sendDocument
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendDocument
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.media.sendDocumentsGroup
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendDocumentsGroup
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.withUploadDocumentAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.telegramBot
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommandWithArgs
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommandWithArgs
|
||||||
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
import dev.inmo.tgbotapi.types.BotCommand
|
import dev.inmo.tgbotapi.types.BotCommand
|
||||||
import dev.inmo.tgbotapi.types.chat.Chat
|
import dev.inmo.tgbotapi.types.chat.Chat
|
||||||
import dev.inmo.tgbotapi.types.files.DocumentFile
|
|
||||||
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
||||||
import dev.inmo.tgbotapi.types.mediaCountInMediaGroup
|
import dev.inmo.tgbotapi.types.mediaCountInMediaGroup
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private const val command = "send_file"
|
private const val command = "send_file"
|
||||||
|
|
||||||
|
expect fun pickFile(currentRoot: MPPFile): MPPFile?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This bot will send files inside of working directory OR from directory in the second argument.
|
* This bot will send files inside of working directory OR from directory in the second argument.
|
||||||
* You may send /send_file command to this bot to get random file from the directory OR
|
* You may send /send_file command to this bot to get random file from the directory OR
|
||||||
@@ -25,19 +27,10 @@ private const val command = "send_file"
|
|||||||
* /send_file and `/send_file 1` will have the same effect - bot will send one random file.
|
* /send_file and `/send_file 1` will have the same effect - bot will send one random file.
|
||||||
* But if you will send `/send_file 5` it will choose 5 random files and send them as group
|
* But if you will send `/send_file 5` it will choose 5 random files and send them as group
|
||||||
*/
|
*/
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun doRandomFileSenderBot(token: String, folder: MPPFile) {
|
||||||
val botToken = args.first()
|
val bot = telegramBot(token)
|
||||||
val directoryOrFile = args.getOrNull(1) ?.let { File(it) } ?: File("")
|
|
||||||
|
|
||||||
fun pickFile(currentRoot: File = directoryOrFile): File? {
|
suspend fun TelegramBot.sendFiles(chat: Chat, files: List<MPPFile>) {
|
||||||
if (currentRoot.isFile) {
|
|
||||||
return currentRoot
|
|
||||||
} else {
|
|
||||||
return pickFile(currentRoot.listFiles() ?.takeIf { it.isNotEmpty() } ?.random() ?: return null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun TelegramBot.sendFiles(chat: Chat, files: List<File>) {
|
|
||||||
when (files.size) {
|
when (files.size) {
|
||||||
1 -> sendDocument(
|
1 -> sendDocument(
|
||||||
chat.id,
|
chat.id,
|
||||||
@@ -52,8 +45,6 @@ suspend fun main(args: Array<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val bot = telegramBot(botToken)
|
|
||||||
|
|
||||||
bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) {
|
bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) {
|
||||||
onCommandWithArgs(command) { message, args ->
|
onCommandWithArgs(command) { message, args ->
|
||||||
|
|
||||||
@@ -62,10 +53,10 @@ suspend fun main(args: Array<String>) {
|
|||||||
var sent = false
|
var sent = false
|
||||||
|
|
||||||
var left = count
|
var left = count
|
||||||
val chosen = mutableListOf<File>()
|
val chosen = mutableListOf<MPPFile>()
|
||||||
|
|
||||||
while (left > 0) {
|
while (left > 0) {
|
||||||
val picked = pickFile() ?.takeIf { it.filesize > 0 } ?: continue
|
val picked = pickFile(folder) ?.takeIf { it.filesize > 0 } ?: continue
|
||||||
chosen.add(picked)
|
chosen.add(picked)
|
||||||
left--
|
left--
|
||||||
if (chosen.size >= mediaCountInMediaGroup.last) {
|
if (chosen.size >= mediaCountInMediaGroup.last) {
|
||||||
10
RandomFileSenderBot/src/jvmMain/kotlin/ActualPickFile.kt
Normal file
10
RandomFileSenderBot/src/jvmMain/kotlin/ActualPickFile.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
actual fun pickFile(currentRoot: MPPFile): File? {
|
||||||
|
if (currentRoot.isFile) {
|
||||||
|
return currentRoot
|
||||||
|
} else {
|
||||||
|
return pickFile(currentRoot.listFiles() ?.takeIf { it.isNotEmpty() } ?.random() ?: return null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
doRandomFileSenderBot(args.first(), MPPFile(args.getOrNull(1) ?: ""))
|
||||||
|
}
|
||||||
10
RandomFileSenderBot/src/nativeMain/kotlin/ActualPickFile.kt
Normal file
10
RandomFileSenderBot/src/nativeMain/kotlin/ActualPickFile.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import dev.inmo.micro_utils.common.MPPFile
|
||||||
|
import okio.FileSystem
|
||||||
|
|
||||||
|
actual fun pickFile(currentRoot: MPPFile): MPPFile? {
|
||||||
|
if (FileSystem.SYSTEM.exists(currentRoot) && FileSystem.SYSTEM.listOrNull(currentRoot) == null) {
|
||||||
|
return currentRoot
|
||||||
|
} else {
|
||||||
|
return pickFile(FileSystem.SYSTEM.list(currentRoot).takeIf { it.isNotEmpty() } ?.random() ?: return null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runBlocking {
|
||||||
|
doRandomFileSenderBot(args.first(), args.getOrNull(1) ?.toPath() ?: "".toPath())
|
||||||
|
}
|
||||||
|
}
|
||||||
41
ReactionsInfoBot/README.md
Normal file
41
ReactionsInfoBot/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# ReactionsInfoBot
|
||||||
|
|
||||||
|
A bot that tracks message reactions and reports them back to the user.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Monitors reaction updates in the bot's private chat. When a user adds or removes a reaction on a
|
||||||
|
message, the bot replies to that message with a formatted summary of the current reactions.
|
||||||
|
The bot also sets a reaction emoji on incoming messages to acknowledge them.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Handles `MessageReactionUpdated` events (per-user reaction changes)
|
||||||
|
- Handles `MessageReactionCountUpdated` events (aggregate reaction counts)
|
||||||
|
- Identifies reaction types: standard emoji, custom emoji, paid reactions
|
||||||
|
- Replies to the reacted-to message with a formatted list of current reactions
|
||||||
|
- Sets a reaction on received messages using `setMessageReaction`
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
ReactionsInfoBot/build.gradle
Normal file
21
ReactionsInfoBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="ReactionsInfoBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
68
ReactionsInfoBot/src/main/kotlin/ReactionsInfoBot.kt
Normal file
68
ReactionsInfoBot/src/main/kotlin/ReactionsInfoBot.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.setMessageReaction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatMessageReactionUpdatedByUser
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatMessageReactionsCountUpdated
|
||||||
|
import dev.inmo.tgbotapi.types.chat.ExtendedChat
|
||||||
|
import dev.inmo.tgbotapi.types.reactions.Reaction
|
||||||
|
import dev.inmo.tgbotapi.utils.customEmoji
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bot will send info about user reactions in his PM with reply to message user reacted to
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.getOrNull(1) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bot = telegramBot(botToken)
|
||||||
|
|
||||||
|
bot.buildBehaviourWithLongPolling {
|
||||||
|
onChatMessageReactionUpdatedByUser {
|
||||||
|
setMessageReaction(
|
||||||
|
it.chat.id,
|
||||||
|
it.messageId,
|
||||||
|
"✍"
|
||||||
|
)
|
||||||
|
val replyResult = reply(
|
||||||
|
it.chat.id,
|
||||||
|
it.messageId,
|
||||||
|
replyInChatId = it.reactedUser.id
|
||||||
|
) {
|
||||||
|
regular("Current reactions for message in reply:\n")
|
||||||
|
it.new.forEach {
|
||||||
|
when (it) {
|
||||||
|
is Reaction.CustomEmoji -> regular("• ") + customEmoji(it.customEmojiId) + regular("(customEmojiId: ${it.customEmojiId})")
|
||||||
|
is Reaction.Emoji -> regular("• ${it.emoji}")
|
||||||
|
is Reaction.Paid -> regular("• Some paid reaction")
|
||||||
|
is Reaction.Unknown -> regular("• Unknown emoji ($it)")
|
||||||
|
}
|
||||||
|
regular("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMessageReaction(
|
||||||
|
it.chat.id,
|
||||||
|
it.messageId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChatMessageReactionsCountUpdated {
|
||||||
|
val extendedChat: ExtendedChat = getChat(it.chat)
|
||||||
|
println(extendedChat)
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
48
ResenderBot/README.md
Normal file
48
ResenderBot/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# ResenderBot
|
||||||
|
|
||||||
|
A multiplatform bot (JVM + Native + JS) that echoes every content message back to the sender.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
For every content message the bot receives, it immediately re-sends the same content back to the
|
||||||
|
originating chat. Reply quotes and message effects are preserved. The bot is implemented as a
|
||||||
|
shared library (`ResenderBotLib`) with separate launcher modules for JVM and Native targets.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional flags (any order):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Re-sends any content message (text, photo, video, audio, document, sticker, etc.)
|
||||||
|
- Preserves `reply_to_message` quote when the original message was a reply
|
||||||
|
- Preserves `effect_id` (message effects / animations)
|
||||||
|
- Shared `commonMain` implementation across JVM, Native, and JS targets
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :ResenderBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Native (after build)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ResenderBot/native_launcher/build/bin/native/releaseExecutable/native_launcher.kexe BOT_TOKEN
|
||||||
|
```
|
||||||
@@ -20,6 +20,9 @@ kotlin {
|
|||||||
browser()
|
browser()
|
||||||
binaries.executable()
|
binaries.executable()
|
||||||
}
|
}
|
||||||
|
linuxX64()
|
||||||
|
mingwX64()
|
||||||
|
linuxArm64()
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
|
|||||||
@@ -1,56 +1,64 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.filter.filtered
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.*
|
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.media.*
|
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
|
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups
|
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
import dev.inmo.tgbotapi.extensions.utils.shortcuts.*
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
import kotlinx.coroutines.*
|
import dev.inmo.tgbotapi.extensions.utils.possiblyWithEffectMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.shortcuts.executeUnsafe
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.BusinessContentMessage
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
|
import dev.inmo.tgbotapi.utils.DefaultKTgBotAPIKSLog
|
||||||
|
import dev.inmo.tgbotapi.utils.extensions.threadIdOrNull
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
|
||||||
suspend fun activateResenderBot(
|
suspend fun activateResenderBot(
|
||||||
token: String,
|
token: String,
|
||||||
print: (Any) -> Unit
|
print: (Any) -> Unit
|
||||||
) {
|
) {
|
||||||
val bot = telegramBot(token)
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
token,
|
||||||
print(bot.getMe())
|
scope = CoroutineScope(currentCoroutineContext() + SupervisorJob()),
|
||||||
|
) {
|
||||||
bot.buildBehaviourWithLongPolling(CoroutineScope(currentCoroutineContext() + SupervisorJob())) {
|
|
||||||
onContentMessage(
|
onContentMessage(
|
||||||
initialFilter = CommonMessageFilterExcludeMediaGroups,
|
subcontextUpdatesFilter = MessageFilterByChat,
|
||||||
subcontextUpdatesFilter = MessageFilterByChat
|
initialFilter = { it !is BusinessContentMessage<*> || !it.sentByBusinessConnectionOwner }
|
||||||
) {
|
) {
|
||||||
val chat = it.chat
|
val chat = it.chat
|
||||||
withTypingAction(chat) {
|
|
||||||
executeUnsafe(it.content.createResend(chat.id, replyToMessageId = it.messageId)) {
|
val answer = withTypingAction(chat) {
|
||||||
|
executeUnsafe(
|
||||||
|
it.content.createResend(
|
||||||
|
chat.id,
|
||||||
|
replyParameters = it.replyInfo?.messageMeta?.let { meta ->
|
||||||
|
val quote = it.withContentOrNull<TextContent>()?.content?.quote
|
||||||
|
ReplyParameters(
|
||||||
|
meta,
|
||||||
|
entities = quote?.textSources ?: emptyList(),
|
||||||
|
quotePosition = quote?.position
|
||||||
|
)
|
||||||
|
},
|
||||||
|
effectId = it.possiblyWithEffectMessageOrNull()?.effectId
|
||||||
|
)
|
||||||
|
) {
|
||||||
it.forEach(print)
|
it.forEach(print)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
onVisualGallery {
|
println("Answer info: $answer")
|
||||||
val chat = it.chat ?: return@onVisualGallery
|
|
||||||
withUploadPhotoAction(chat) {
|
|
||||||
send(chat, it.map { it.content.toMediaGroupMemberTelegramMedia() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPlaylist {
|
|
||||||
val chat = it.chat ?: return@onPlaylist
|
|
||||||
withUploadDocumentAction(chat) {
|
|
||||||
send(chat, it.map { it.content.toMediaGroupMemberTelegramMedia() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDocumentsGroup {
|
|
||||||
val chat = it.chat ?: return@onDocumentsGroup
|
|
||||||
withUploadDocumentAction(chat) {
|
|
||||||
send(chat, it.map { it.content.toMediaGroupMemberTelegramMedia() })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
}.join()
|
print(bot.getMe())
|
||||||
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,19 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun main(args: Array<String>) {
|
||||||
|
val isDebug = args.getOrNull(1) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
activateResenderBot(args.first()) {
|
activateResenderBot(args.first()) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
|
|||||||
28
ResenderBot/native_launcher/build.gradle
Normal file
28
ResenderBot/native_launcher/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$nativePartTemplate"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation kotlin('stdlib')
|
||||||
|
|
||||||
|
api project(":ResenderBot:ResenderBotLib")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
fun main(vararg args: String) {
|
||||||
|
runBlocking {
|
||||||
|
activateResenderBot(args.first()) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
RightsChangerBot/README.md
Normal file
50
RightsChangerBot/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# RightsChangerBot
|
||||||
|
|
||||||
|
A bot for managing user permissions and administrator rights in Telegram groups and channels.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Provides two modes of permission editing (simple / granular) for regular member restrictions, and a
|
||||||
|
full FSM-based flow for editing channel administrator rights. Changes are presented as inline
|
||||||
|
keyboards with visual ✅/❌ toggles that persist until the user is done.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
| 2 | `ADMIN_USER_ID` | Numeric Telegram user ID allowed to use the bot |
|
||||||
|
|
||||||
|
Optional flags (any order after the required arguments):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/simple` | Show a common-permissions keyboard (send messages, polls, web previews, etc.) for the replied-to user |
|
||||||
|
| `/granular` | Show a granular-permissions keyboard (individual media types) for the replied-to user |
|
||||||
|
| `/rights_in_channel` | Start the FSM flow to pick a channel and a user, then edit that user's administrator rights in the channel |
|
||||||
|
|
||||||
|
All commands must be sent as a **reply** to a target user's message.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- **Simple mode** — toggles grouped permissions: send messages, send media, send polls, send other content, add web page previews, change info, invite users, pin messages
|
||||||
|
- **Granular mode** — toggles individual media-type permissions: audios, documents, photos, videos, video notes, voice notes, stickers, animations, games, gift premiums, forward channels, forward non-channels
|
||||||
|
- **Channel admin rights** — FSM with three states:
|
||||||
|
1. `RetrievingChannelChatState` — user picks the channel
|
||||||
|
2. `RetrievingUserIdChatState` — user picks the member
|
||||||
|
3. `RetrievingChatInfoDoneState` — inline keyboard for toggling admin rights (post messages, edit messages, delete messages, ban users, invite users, pin messages, manage topics, manage video chats, post stories, edit stories, delete stories, remain anonymous)
|
||||||
|
- Inline keyboard callbacks update permission state in real time
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN ADMIN_USER_ID"
|
||||||
|
```
|
||||||
22
RightsChangerBot/build.gradle
Normal file
22
RightsChangerBot/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="RightsChangerKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
implementation 'io.ktor:ktor-client-logging-jvm:3.2.3'
|
||||||
|
}
|
||||||
540
RightsChangerBot/src/main/kotlin/RightsChanger.kt
Normal file
540
RightsChangerBot/src/main/kotlin/RightsChanger.kt
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.firstOf
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.micro_utils.fsm.common.State
|
||||||
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.members.promoteChannelAdministrator
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.members.restrictChatMember
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithFSMAndStartLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitChatSharedEventsMessages
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommandMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitUserSharedEventsMessages
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
|
||||||
|
import dev.inmo.tgbotapi.types.*
|
||||||
|
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
||||||
|
import dev.inmo.tgbotapi.types.chat.ChannelChat
|
||||||
|
import dev.inmo.tgbotapi.types.chat.ChatPermissions
|
||||||
|
import dev.inmo.tgbotapi.types.chat.PublicChat
|
||||||
|
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
|
||||||
|
import dev.inmo.tgbotapi.types.chat.member.ChatCommonAdministratorRights
|
||||||
|
import dev.inmo.tgbotapi.types.commands.BotCommandScope
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.AccessibleMessage
|
||||||
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
|
import dev.inmo.tgbotapi.utils.*
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
|
||||||
|
sealed interface UserRetrievingStep : State {
|
||||||
|
data class RetrievingChannelChatState(
|
||||||
|
override val context: ChatId
|
||||||
|
) : UserRetrievingStep
|
||||||
|
data class RetrievingUserIdChatState(
|
||||||
|
override val context: ChatId,
|
||||||
|
val channelId: ChatId
|
||||||
|
) : UserRetrievingStep
|
||||||
|
data class RetrievingChatInfoDoneState(
|
||||||
|
override val context: ChatId,
|
||||||
|
val channelId: ChatId,
|
||||||
|
val userId: UserId
|
||||||
|
) : UserRetrievingStep
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(PreviewFeature::class)
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.getOrNull(2) == "debug"
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bot = telegramBot(botToken)
|
||||||
|
|
||||||
|
val allowedAdmin = ChatId(RawChatId(args[1].toLong()))
|
||||||
|
|
||||||
|
fun Boolean?.allowedSymbol() = when (this) {
|
||||||
|
true -> "✅"
|
||||||
|
false -> "❌"
|
||||||
|
null -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val granularDataPrefix = "granular"
|
||||||
|
val messagesToggleGranularData = "$granularDataPrefix messages"
|
||||||
|
val otherMessagesToggleGranularData = "$granularDataPrefix other messages"
|
||||||
|
val audiosToggleGranularData = "$granularDataPrefix audios"
|
||||||
|
val voicesToggleGranularData = "$granularDataPrefix voices"
|
||||||
|
val videosToggleGranularData = "$granularDataPrefix videos"
|
||||||
|
val videoNotesToggleGranularData = "$granularDataPrefix video notes"
|
||||||
|
val photosToggleGranularData = "$granularDataPrefix photos"
|
||||||
|
val webPagePreviewToggleGranularData = "$granularDataPrefix web page preview"
|
||||||
|
val pollsToggleGranularData = "$granularDataPrefix polls"
|
||||||
|
val documentsToggleGranularData = "$granularDataPrefix documents"
|
||||||
|
|
||||||
|
val commonDataPrefix = "common"
|
||||||
|
val pollsToggleCommonData = "$commonDataPrefix polls"
|
||||||
|
val otherMessagesToggleCommonData = "$commonDataPrefix other messages"
|
||||||
|
val webPagePreviewToggleCommonData = "$commonDataPrefix web page preview"
|
||||||
|
|
||||||
|
val adminRightsDataPrefix = "admin"
|
||||||
|
val refreshAdminRightsData = "${adminRightsDataPrefix}_refresh"
|
||||||
|
val postMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_post_messages"
|
||||||
|
val editMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_messages"
|
||||||
|
val deleteMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_messages"
|
||||||
|
val editStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_stories"
|
||||||
|
val deleteStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_stories"
|
||||||
|
val postStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_post_stories"
|
||||||
|
|
||||||
|
suspend fun BehaviourContext.getUserChatPermissions(chatId: ChatId, userId: UserId): ChatPermissions? {
|
||||||
|
val chatMember = getChatMember(chatId, userId)
|
||||||
|
return chatMember.restrictedMemberChatMemberOrNull() ?: chatMember.whenMemberChatMember {
|
||||||
|
getChat(chatId).extendedGroupChatOrNull() ?.permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun buildGranularKeyboard(
|
||||||
|
permissions: ChatPermissions
|
||||||
|
): InlineKeyboardMarkup {
|
||||||
|
return inlineKeyboard {
|
||||||
|
row {
|
||||||
|
dataButton("Send messages${permissions.canSendMessages.allowedSymbol()}", messagesToggleGranularData)
|
||||||
|
dataButton(
|
||||||
|
"Send other messages${permissions.canSendOtherMessages.allowedSymbol()}",
|
||||||
|
otherMessagesToggleGranularData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Send audios${permissions.canSendAudios.allowedSymbol()}", audiosToggleGranularData)
|
||||||
|
dataButton("Send voices${permissions.canSendVoiceNotes.allowedSymbol()}", voicesToggleGranularData)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Send videos${permissions.canSendVideos.allowedSymbol()}", videosToggleGranularData)
|
||||||
|
dataButton(
|
||||||
|
"Send video notes${permissions.canSendVideoNotes.allowedSymbol()}",
|
||||||
|
videoNotesToggleGranularData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Send photos${permissions.canSendPhotos.allowedSymbol()}", photosToggleGranularData)
|
||||||
|
dataButton(
|
||||||
|
"Add web preview${permissions.canAddWebPagePreviews.allowedSymbol()}",
|
||||||
|
webPagePreviewToggleGranularData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Send polls${permissions.canSendPolls.allowedSymbol()}", pollsToggleGranularData)
|
||||||
|
dataButton("Send documents${permissions.canSendDocuments.allowedSymbol()}", documentsToggleGranularData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun buildAdminRightsKeyboard(
|
||||||
|
permissions: AdministratorChatMember?,
|
||||||
|
channelId: ChatId,
|
||||||
|
userId: UserId
|
||||||
|
): InlineKeyboardMarkup {
|
||||||
|
return inlineKeyboard {
|
||||||
|
permissions ?.also {
|
||||||
|
row {
|
||||||
|
dataButton("Refresh", "$refreshAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Edit messages${permissions.canEditMessages.allowedSymbol()}", "$editMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
dataButton("Delete messages${permissions.canRemoveMessages.allowedSymbol()}", "$deleteMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Post messages${permissions.canPostMessages.allowedSymbol()}", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Edit stories${permissions.canEditStories.allowedSymbol()}", "$editStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
dataButton("Delete stories${permissions.canDeleteStories.allowedSymbol()}", "$deleteStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Post stories${permissions.canPostStories.allowedSymbol()}", "$postStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
} ?: row {
|
||||||
|
dataButton("Promote to admin", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun BehaviourContext.buildGranularKeyboard(chatId: ChatId, userId: UserId): InlineKeyboardMarkup? {
|
||||||
|
return buildGranularKeyboard(
|
||||||
|
getUserChatPermissions(chatId, userId) ?: return null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun BehaviourContext.buildCommonKeyboard(chatId: ChatId, userId: UserId): InlineKeyboardMarkup? {
|
||||||
|
val permissions = getUserChatPermissions(chatId, userId) ?: return null
|
||||||
|
|
||||||
|
return inlineKeyboard {
|
||||||
|
row {
|
||||||
|
dataButton("Send polls${permissions.canSendPolls.allowedSymbol()}", pollsToggleCommonData)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Send other messages${permissions.canSendOtherMessages.allowedSymbol()}", otherMessagesToggleCommonData)
|
||||||
|
}
|
||||||
|
row {
|
||||||
|
dataButton("Add web preview${permissions.canAddWebPagePreviews.allowedSymbol()}", webPagePreviewToggleCommonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.buildBehaviourWithFSMAndStartLongPolling<UserRetrievingStep>(
|
||||||
|
defaultExceptionsHandler = {
|
||||||
|
it.printStackTrace()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
onCommand(
|
||||||
|
"simple",
|
||||||
|
initialFilter = { it.chat is PublicChat && it.fromUserMessageOrNull()?.user?.id == allowedAdmin }
|
||||||
|
) {
|
||||||
|
val replyMessage = it.replyTo
|
||||||
|
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||||
|
if (replyMessage is AccessibleMessage) {
|
||||||
|
reply(
|
||||||
|
replyMessage,
|
||||||
|
"Manage keyboard:",
|
||||||
|
replyMarkup = buildCommonKeyboard(it.chat.id.toChatId(), userInReply) ?: return@onCommand
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reply(it) {
|
||||||
|
regular("Reply to somebody's message to get hist/her rights keyboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand(
|
||||||
|
"granular",
|
||||||
|
initialFilter = {
|
||||||
|
it.chat is ChannelChat || (it.chat is PublicChat && it.fromUserMessageOrNull()?.user?.id == allowedAdmin)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val replyMessage = it.replyTo
|
||||||
|
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||||
|
|
||||||
|
if (replyMessage is AccessibleMessage) {
|
||||||
|
reply(
|
||||||
|
replyMessage,
|
||||||
|
"Manage keyboard:",
|
||||||
|
replyMarkup = buildGranularKeyboard(it.chat.id.toChatId(), userInReply) ?: return@onCommand
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reply(it) {
|
||||||
|
regular("Reply to somebody's message to get hist/her rights keyboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageDataCallbackQuery(
|
||||||
|
Regex("^${granularDataPrefix}.*"),
|
||||||
|
initialFilter = { it.user.id == allowedAdmin }
|
||||||
|
) {
|
||||||
|
val messageReply =
|
||||||
|
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||||
|
val userId = messageReply.user.id
|
||||||
|
val permissions =
|
||||||
|
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val newPermission = when (it.data) {
|
||||||
|
messagesToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendMessages = permissions.canSendMessages?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherMessagesToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendOtherMessages = permissions.canSendOtherMessages?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
audiosToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendAudios = permissions.canSendAudios?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
voicesToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendVoiceNotes = permissions.canSendVoiceNotes?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
videosToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendVideos = permissions.canSendVideos?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoNotesToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendVideoNotes = permissions.canSendVideoNotes?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
photosToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendPhotos = permissions.canSendPhotos?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
webPagePreviewToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canAddWebPagePreviews = permissions.canAddWebPagePreviews?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pollsToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendPolls = permissions.canSendPolls?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
documentsToggleGranularData -> {
|
||||||
|
permissions.copyGranular(
|
||||||
|
canSendDocuments = permissions.canSendDocuments?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> permissions.copyGranular()
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictChatMember(
|
||||||
|
it.message.chat.id,
|
||||||
|
userId,
|
||||||
|
permissions = newPermission,
|
||||||
|
useIndependentChatPermissions = true
|
||||||
|
)
|
||||||
|
|
||||||
|
edit(
|
||||||
|
it.message,
|
||||||
|
replyMarkup = buildGranularKeyboard(it.message.chat.id.toChatId(), userId)
|
||||||
|
?: return@onMessageDataCallbackQuery
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageDataCallbackQuery(
|
||||||
|
Regex("^${commonDataPrefix}.*"),
|
||||||
|
initialFilter = { it.user.id == allowedAdmin }
|
||||||
|
) {
|
||||||
|
val messageReply =
|
||||||
|
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||||
|
val userId = messageReply.user.id
|
||||||
|
val permissions =
|
||||||
|
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||||
|
val newPermission = when (it.data) {
|
||||||
|
pollsToggleCommonData -> {
|
||||||
|
permissions.copyCommon(
|
||||||
|
canSendPolls = permissions.canSendPolls?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherMessagesToggleCommonData -> {
|
||||||
|
permissions.copyCommon(
|
||||||
|
canSendOtherMessages = permissions.canSendOtherMessages?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
webPagePreviewToggleCommonData -> {
|
||||||
|
permissions.copyCommon(
|
||||||
|
canAddWebPagePreviews = permissions.canAddWebPagePreviews?.let { !it } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> permissions.copyCommon()
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictChatMember(
|
||||||
|
it.message.chat.id,
|
||||||
|
userId,
|
||||||
|
permissions = newPermission,
|
||||||
|
useIndependentChatPermissions = false
|
||||||
|
)
|
||||||
|
|
||||||
|
edit(
|
||||||
|
it.message,
|
||||||
|
replyMarkup = buildCommonKeyboard(it.message.chat.id.toChatId(), userId)
|
||||||
|
?: return@onMessageDataCallbackQuery
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageDataCallbackQuery(
|
||||||
|
Regex("^${adminRightsDataPrefix}.*"),
|
||||||
|
initialFilter = { it.user.id == allowedAdmin }
|
||||||
|
) {
|
||||||
|
val (channelIdString, userIdString) = it.data.split(" ").drop(1)
|
||||||
|
val channelId = ChatId(RawChatId(channelIdString.toLong()))
|
||||||
|
val userId = ChatId(RawChatId(userIdString.toLong()))
|
||||||
|
val chatMember = getChatMember(channelId, userId)
|
||||||
|
val asAdmin = chatMember.administratorChatMemberOrNull()
|
||||||
|
|
||||||
|
val realData = it.data.takeWhile { it != ' ' }
|
||||||
|
|
||||||
|
fun Boolean?.toggleIfData(data: String) = if (realData == data) {
|
||||||
|
!(this ?: false)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realData != refreshAdminRightsData) {
|
||||||
|
promoteChannelAdministrator(
|
||||||
|
channelId,
|
||||||
|
userId,
|
||||||
|
canPostMessages = asAdmin ?.canPostMessages.toggleIfData(postMessagesToggleAdminRightsData),
|
||||||
|
canEditMessages = asAdmin ?.canEditMessages.toggleIfData(editMessagesToggleAdminRightsData),
|
||||||
|
canDeleteMessages = asAdmin ?.canRemoveMessages.toggleIfData(deleteMessagesToggleAdminRightsData),
|
||||||
|
canEditStories = asAdmin ?.canEditStories.toggleIfData(editStoriesToggleAdminRightsData),
|
||||||
|
canDeleteStories = asAdmin ?.canDeleteStories.toggleIfData(deleteStoriesToggleAdminRightsData),
|
||||||
|
canPostStories = asAdmin ?.canPostStories.toggleIfData(postStoriesToggleAdminRightsData),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(
|
||||||
|
it.message,
|
||||||
|
replyMarkup = buildAdminRightsKeyboard(
|
||||||
|
getChatMember(
|
||||||
|
channelId,
|
||||||
|
userId
|
||||||
|
).administratorChatMemberOrNull(),
|
||||||
|
channelId,
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
strictlyOn<UserRetrievingStep.RetrievingChannelChatState> { state ->
|
||||||
|
val requestId = RequestId.random()
|
||||||
|
send(
|
||||||
|
state.context,
|
||||||
|
replyMarkup = replyKeyboard(
|
||||||
|
oneTimeKeyboard = true,
|
||||||
|
resizeKeyboard = true
|
||||||
|
) {
|
||||||
|
row {
|
||||||
|
requestChatButton(
|
||||||
|
"Choose channel",
|
||||||
|
requestId = requestId,
|
||||||
|
isChannel = true,
|
||||||
|
botIsMember = true,
|
||||||
|
botRightsInChat = ChatCommonAdministratorRights(
|
||||||
|
canPromoteMembers = true,
|
||||||
|
canRestrictMembers = true
|
||||||
|
),
|
||||||
|
userRightsInChat = ChatCommonAdministratorRights(
|
||||||
|
canPromoteMembers = true,
|
||||||
|
canRestrictMembers = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
regular("Ok, send me the channel in which you wish to manage user, or use ")
|
||||||
|
botCommand("cancel")
|
||||||
|
regular(" to cancel the request")
|
||||||
|
}
|
||||||
|
firstOf {
|
||||||
|
include {
|
||||||
|
val chatId = waitChatSharedEventsMessages().mapNotNull {
|
||||||
|
it.chatEvent.chatId.takeIf { _ ->
|
||||||
|
it.chatEvent.requestId == requestId && it.sameChat(state.context)
|
||||||
|
}
|
||||||
|
}.first()
|
||||||
|
UserRetrievingStep.RetrievingUserIdChatState(state.context, chatId)
|
||||||
|
}
|
||||||
|
include {
|
||||||
|
waitCommandMessage("cancel").filter { it.sameChat(state.context) }.first()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strictlyOn<UserRetrievingStep.RetrievingUserIdChatState> { state ->
|
||||||
|
val requestId = RequestId.random()
|
||||||
|
send(
|
||||||
|
state.context,
|
||||||
|
replyMarkup = replyKeyboard(
|
||||||
|
oneTimeKeyboard = true,
|
||||||
|
resizeKeyboard = true
|
||||||
|
) {
|
||||||
|
row {
|
||||||
|
requestUserButton(
|
||||||
|
"Choose user",
|
||||||
|
requestId = requestId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
regular("Ok, send me the user for which you wish to change rights, or use ")
|
||||||
|
botCommand("cancel")
|
||||||
|
regular(" to cancel the request")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstOf {
|
||||||
|
include {
|
||||||
|
val userContactChatId = waitUserSharedEventsMessages().filter {
|
||||||
|
it.sameChat(state.context)
|
||||||
|
}.first().chatEvent.chatId
|
||||||
|
UserRetrievingStep.RetrievingChatInfoDoneState(
|
||||||
|
state.context,
|
||||||
|
state.channelId,
|
||||||
|
userContactChatId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
include {
|
||||||
|
waitCommandMessage("cancel").filter { it.sameChat(state.context) }.first()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strictlyOn<UserRetrievingStep.RetrievingChatInfoDoneState> { state ->
|
||||||
|
val chatMember = getChatMember(state.channelId, state.userId).administratorChatMemberOrNull()
|
||||||
|
if (chatMember == null) {
|
||||||
|
return@strictlyOn null
|
||||||
|
}
|
||||||
|
send(
|
||||||
|
state.context,
|
||||||
|
replyMarkup = buildAdminRightsKeyboard(
|
||||||
|
chatMember,
|
||||||
|
state.channelId,
|
||||||
|
state.userId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
regular("Rights of ")
|
||||||
|
mentionln(chatMember.user)
|
||||||
|
regular("Please, remember, that to be able to change user rights bot must promote user by itself to admin")
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("rights_in_channel") {
|
||||||
|
startChain(UserRetrievingStep.RetrievingChannelChatState(it.chat.id.toChatId()))
|
||||||
|
}
|
||||||
|
|
||||||
|
setMyCommands(
|
||||||
|
BotCommand("simple", "Trigger simple keyboard. Use with reply to user"),
|
||||||
|
BotCommand("granular", "Trigger granular keyboard. Use with reply to user"),
|
||||||
|
BotCommand("rights_in_channel", "Trigger granular keyboard. Use with reply to user"),
|
||||||
|
scope = BotCommandScope.AllGroupChats
|
||||||
|
)
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
}
|
||||||
@@ -1,6 +1,35 @@
|
|||||||
# SlotMachineDetectorBot
|
# SlotMachineDetectorBot
|
||||||
|
|
||||||
This bot must reply with information about slot machine answer
|
A bot that detects slot-machine dice rolls and reports the result.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Listens for dice messages of the *SlotMachine* type. When one is received, it calculates the
|
||||||
|
combination shown on the three reels and replies with the formatted result.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Filters incoming dice messages specifically for the slot-machine emoji type
|
||||||
|
- Decodes the numeric dice value into the three reel symbols
|
||||||
|
- Replies with a human-readable description of the result
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
|
|||||||
46
StarTransactionsBot/README.md
Normal file
46
StarTransactionsBot/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# StarTransactionsBot
|
||||||
|
|
||||||
|
A bot that demonstrates Telegram Stars payments: sending invoices, handling transactions, and
|
||||||
|
delivering paid media.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Sends a 1-star invoice on `/start`. After successful payment the bot sends paid media (a photo
|
||||||
|
and a video). The admin can browse the full transaction history with pagination. Refunds received
|
||||||
|
from Telegram are logged. Checkout queries are validated before approval.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
| 2 | `ADMIN_USER_ID` | Numeric Telegram user ID that is allowed to view transaction history |
|
||||||
|
|
||||||
|
Optional flags (any order after the required arguments):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Send a 1-star invoice to the user |
|
||||||
|
| `/transactions` | Browse paginated star transaction history *(admin only)* |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Creates and sends a Stars invoice via `sendInvoice`
|
||||||
|
- Handles `PreCheckoutQuery` events to approve or reject checkout
|
||||||
|
- Delivers paid media (photo + video) after a successful payment
|
||||||
|
- Paginates transaction history using inline keyboard next/previous buttons
|
||||||
|
- Tracks and logs refund notifications
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN ADMIN_USER_ID"
|
||||||
|
```
|
||||||
21
StarTransactionsBot/build.gradle
Normal file
21
StarTransactionsBot/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="StarTransactionsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
185
StarTransactionsBot/src/main/kotlin/StarTransactionsBot.kt
Normal file
185
StarTransactionsBot/src/main/kotlin/StarTransactionsBot.kt
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.answers.payments.answerPreCheckoutQueryOk
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.get.getStarTransactions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
|
import dev.inmo.tgbotapi.types.ChatId
|
||||||
|
import dev.inmo.tgbotapi.types.RawChatId
|
||||||
|
import dev.inmo.tgbotapi.types.UserId
|
||||||
|
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
||||||
|
import dev.inmo.tgbotapi.types.files.*
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramPaidMediaPhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramPaidMediaVideo
|
||||||
|
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaVideo
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
||||||
|
import dev.inmo.tgbotapi.types.payments.LabeledPrice
|
||||||
|
import dev.inmo.tgbotapi.types.payments.stars.StarTransaction
|
||||||
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
|
import dev.inmo.tgbotapi.utils.bold
|
||||||
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
|
import dev.inmo.tgbotapi.utils.row
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An example bot that interacts with Telegram Stars API (used for payments)
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val adminUserId = args.getOrNull(1) ?.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: error("Pass user-admin for full access to the bot")
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO), testServer = isTestServer) {
|
||||||
|
val payload = "sample payload"
|
||||||
|
command("start") {
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
price = LabeledPrice("1", 1L),
|
||||||
|
title = "Sample",
|
||||||
|
description = "Sample description",
|
||||||
|
payload = payload,
|
||||||
|
replyMarkup = flatInlineKeyboard {
|
||||||
|
payButton("Pay")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreCheckoutQuery(initialFilter = { it.invoicePayload == payload }) {
|
||||||
|
answerPreCheckoutQueryOk(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val transactionsDataPrefix = "getStarTransactions"
|
||||||
|
fun buildTransactionsData(offset: Int, limit: Int = 10) = "$transactionsDataPrefix $offset $limit"
|
||||||
|
fun parseTransactionsData(data: String): Pair<Int, Int> = data.split(" ").drop(1).let {
|
||||||
|
it.first().toInt() to it.last().toInt()
|
||||||
|
}
|
||||||
|
suspend fun buildStarTransactionsPage(offset: Int, limit: Int = 10): Pair<TextSourcesList, InlineKeyboardMarkup> {
|
||||||
|
val transactions = getStarTransactions(offset, limit)
|
||||||
|
return buildEntities {
|
||||||
|
transactions.transactions.forEach {
|
||||||
|
regular("Transaction Id: ") + bold(it.id.string) + "\n"
|
||||||
|
regular("Date: ") + bold(it.date.asDate.toStringDefault()) + "\n"
|
||||||
|
regular("Amount: ") + bold(it.amount.toString()) + "\n"
|
||||||
|
when (it) {
|
||||||
|
is StarTransaction.Incoming -> {
|
||||||
|
regular("Type: ") + bold("incoming") + "\n"
|
||||||
|
regular("Partner: ") + bold(it.partner.type) + "\n"
|
||||||
|
}
|
||||||
|
is StarTransaction.Outgoing -> {
|
||||||
|
regular("Type: ") + bold("outgoing") + "\n"
|
||||||
|
regular("Partner: ") + bold(it.partner.type) + "\n"
|
||||||
|
}
|
||||||
|
is StarTransaction.Unknown -> {
|
||||||
|
regular("Type: ") + bold("unknown") + "\n"
|
||||||
|
regular("Partner: ") + bold(it.partner.type) + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} to inlineKeyboard {
|
||||||
|
row {
|
||||||
|
val prevOffset = (offset - limit).coerceAtLeast(0)
|
||||||
|
if (prevOffset < offset) {
|
||||||
|
dataButton("<", buildTransactionsData(prevOffset, limit))
|
||||||
|
}
|
||||||
|
val nextOffset = (offset + limit)
|
||||||
|
dataButton(">", buildTransactionsData(nextOffset, limit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCommand("transactions", initialFilter = { it.sameChat(adminUserId) }) {
|
||||||
|
val (text, keyboard) = buildStarTransactionsPage(0)
|
||||||
|
|
||||||
|
reply(it, text, replyMarkup = keyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessageDataCallbackQuery(Regex("$transactionsDataPrefix \\d+ \\d+")) {
|
||||||
|
val (offset, limit) = parseTransactionsData(it.data)
|
||||||
|
val (text, keyboard) = buildStarTransactionsPage(offset, limit)
|
||||||
|
edit(
|
||||||
|
it.message.withContentOrNull<TextContent>() ?: return@onMessageDataCallbackQuery,
|
||||||
|
text,
|
||||||
|
replyMarkup = keyboard,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisualGalleryMessages {
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
1,
|
||||||
|
it.content.group.mapNotNull {
|
||||||
|
val file = downloadFileToTemp(it.content.media)
|
||||||
|
when (it.content.media) {
|
||||||
|
is VideoFile -> {
|
||||||
|
TelegramPaidMediaVideo(
|
||||||
|
file.asMultipartFile()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PhotoSize -> {
|
||||||
|
TelegramPaidMediaPhoto(
|
||||||
|
file.asMultipartFile()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
it.content.textSources,
|
||||||
|
showCaptionAboveMedia = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPhoto {
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
1,
|
||||||
|
listOf(it.content.media.toTelegramPaidMediaPhoto())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVideo {
|
||||||
|
send(
|
||||||
|
it.chat,
|
||||||
|
1,
|
||||||
|
listOf(it.content.media.toTelegramPaidMediaVideo())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPaidMediaInfoContent {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRefundedPayment {
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
"Received your refund: ${it.chatEvent.payment}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
44
StickerInfoBot/README.md
Normal file
44
StickerInfoBot/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# StickerInfoBot
|
||||||
|
|
||||||
|
A multiplatform bot (JVM + JS) that displays detailed information about stickers and custom emoji.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
When the user sends a sticker, the bot replies with the sticker set name, title, and sticker type.
|
||||||
|
When the user sends a text message containing custom emoji entities, the bot fetches the
|
||||||
|
corresponding sticker objects and sends back their information.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional flags (any order):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Detects incoming sticker messages and calls `getStickerSet` to retrieve set metadata
|
||||||
|
- Reports sticker set name, title, and type (regular / mask / custom emoji)
|
||||||
|
- Scans text message entities for `CustomEmoji` types
|
||||||
|
- Fetches the corresponding sticker objects via `getCustomEmojiStickers`
|
||||||
|
- Sends sticker information back as a formatted reply
|
||||||
|
- Shared `commonMain` library with JVM and JS launchers
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :StickerInfoBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandler
|
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandler
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
|
||||||
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
import dev.inmo.tgbotapi.bot.ktor.telegramBot
|
||||||
import dev.inmo.tgbotapi.extensions.api.get.*
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.*
|
import dev.inmo.tgbotapi.extensions.api.get.getCustomEmojiStickerOrNull
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
|
import dev.inmo.tgbotapi.extensions.api.get.getStickerSet
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
import dev.inmo.tgbotapi.extensions.api.get.getStickerSetOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSticker
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onText
|
||||||
import dev.inmo.tgbotapi.types.StickerType
|
import dev.inmo.tgbotapi.types.StickerType
|
||||||
import dev.inmo.tgbotapi.types.message.textsources.*
|
import dev.inmo.tgbotapi.types.message.textsources.CustomEmojiTextSource
|
||||||
|
import dev.inmo.tgbotapi.types.message.textsources.regularTextSource
|
||||||
|
import dev.inmo.tgbotapi.types.message.textsources.separateForText
|
||||||
import dev.inmo.tgbotapi.types.stickers.StickerSet
|
import dev.inmo.tgbotapi.types.stickers.StickerSet
|
||||||
import dev.inmo.tgbotapi.utils.bold
|
import dev.inmo.tgbotapi.utils.bold
|
||||||
import dev.inmo.tgbotapi.utils.buildEntities
|
import dev.inmo.tgbotapi.utils.buildEntities
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
|
||||||
fun StickerSet?.buildInfo() = buildEntities {
|
fun StickerSet?.buildInfo() = buildEntities {
|
||||||
if (this@buildInfo == null) {
|
if (this@buildInfo == null) {
|
||||||
@@ -26,7 +34,7 @@ fun StickerSet?.buildInfo() = buildEntities {
|
|||||||
StickerType.Regular -> "Regular"
|
StickerType.Regular -> "Regular"
|
||||||
is StickerType.Unknown -> "Unknown type \"${stickerType.type}\""
|
is StickerType.Unknown -> "Unknown type \"${stickerType.type}\""
|
||||||
}
|
}
|
||||||
) + " sticker set with title " + bold(title) + " and name " + bold(name)
|
) + " sticker set with title " + bold(title) + " and name " + bold(name.string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +55,14 @@ suspend fun activateStickerInfoBot(
|
|||||||
withTypingAction(it.chat) {
|
withTypingAction(it.chat) {
|
||||||
it.content.textSources.mapNotNull {
|
it.content.textSources.mapNotNull {
|
||||||
if (it is CustomEmojiTextSource) {
|
if (it is CustomEmojiTextSource) {
|
||||||
getCustomEmojiStickerOrNull(it.customEmojiId) ?.stickerSetName
|
getCustomEmojiStickerOrNull(it.customEmojiId)?.stickerSetName
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.distinct().map {
|
}.distinct().map {
|
||||||
getStickerSet(it)
|
getStickerSet(it)
|
||||||
}.distinct().flatMap {
|
}.distinct().flatMap {
|
||||||
it.buildInfo() + regular("\n")
|
it.buildInfo() + regularTextSource("\n")
|
||||||
}.separateForText().map { entities ->
|
}.separateForText().map { entities ->
|
||||||
reply(it, entities)
|
reply(it, entities)
|
||||||
}
|
}
|
||||||
@@ -68,7 +76,7 @@ suspend fun activateStickerInfoBot(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
}.join()
|
}.join()
|
||||||
|
|||||||
46
StickerSetHandler/README.md
Normal file
46
StickerSetHandler/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# StickerSetHandler
|
||||||
|
|
||||||
|
A bot that builds and manages a personal sticker set for each user from stickers they send.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
When a user sends a sticker, the bot extracts its emoji and adds it to a per-user sticker set
|
||||||
|
named `<user_id>_by_<bot_username>`. If the set does not yet exist, it is created first. The bot
|
||||||
|
supports regular, mask, and custom emoji sticker sets, determined by the type of the first sticker
|
||||||
|
added. Sending `/delete` removes the user's entire sticker set.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Sends a welcome message explaining how to use the bot |
|
||||||
|
| `/delete` | Deletes the user's personal sticker set created by this bot |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Per-user sticker set with a deterministic name based on user ID and bot username
|
||||||
|
- Automatic sticker set creation on first sticker received
|
||||||
|
- Supports all sticker set types: regular, mask, custom emoji
|
||||||
|
- Emoji extraction from incoming stickers
|
||||||
|
- Sticker added to an existing set via `addStickerToSet`
|
||||||
|
- Set deletion via `deleteStickerSet`
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
StickerSetHandler/build.gradle
Normal file
21
StickerSetHandler/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="StickerSetHandlerBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
122
StickerSetHandler/src/main/kotlin/StickerSetHandlerBot.kt
Normal file
122
StickerSetHandler/src/main/kotlin/StickerSetHandlerBot.kt
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.get.getStickerSet
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.stickers.addStickerToSet
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.stickers.createNewStickerSet
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.stickers.deleteStickerSet
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSticker
|
||||||
|
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||||
|
import dev.inmo.tgbotapi.requests.stickers.InputSticker
|
||||||
|
import dev.inmo.tgbotapi.types.StickerSetName
|
||||||
|
import dev.inmo.tgbotapi.types.chat.Chat
|
||||||
|
import dev.inmo.tgbotapi.types.files.CustomEmojiSticker
|
||||||
|
import dev.inmo.tgbotapi.types.files.MaskSticker
|
||||||
|
import dev.inmo.tgbotapi.types.files.RegularSticker
|
||||||
|
import dev.inmo.tgbotapi.types.files.UnknownSticker
|
||||||
|
import dev.inmo.tgbotapi.types.toChatId
|
||||||
|
import dev.inmo.tgbotapi.utils.botCommand
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send sticker to this bot to form your own stickers set. Send /delete to delete this sticker set
|
||||||
|
*/
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
args.first(),
|
||||||
|
scope = CoroutineScope(Dispatchers.IO),
|
||||||
|
defaultExceptionsHandler = {
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val me = getMe()
|
||||||
|
fun Chat.stickerSetName() = StickerSetName("s${id.chatId}_by_${me.username ?.withoutAt}")
|
||||||
|
onCommand("start") {
|
||||||
|
reply(it) {
|
||||||
|
botCommand("delete") + " - to clear stickers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCommand("delete") {
|
||||||
|
val deleted = runCatchingSafely {
|
||||||
|
deleteStickerSet(it.chat.stickerSetName())
|
||||||
|
}.map {
|
||||||
|
true
|
||||||
|
}.getOrElse {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleted) {
|
||||||
|
reply(it, "Deleted")
|
||||||
|
} else {
|
||||||
|
reply(it, "Can't delete for some of reason")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSticker {
|
||||||
|
val stickerSetName = it.chat.stickerSetName()
|
||||||
|
val sticker = it.content.media
|
||||||
|
val newSticker = when (sticker) {
|
||||||
|
is CustomEmojiSticker -> InputSticker.WithKeywords.CustomEmoji(
|
||||||
|
downloadFileToTemp(sticker.fileId).asMultipartFile(),
|
||||||
|
sticker.stickerFormat,
|
||||||
|
listOf(sticker.emoji ?: "\uD83D\uDE0A"),
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
is MaskSticker -> InputSticker.Mask(
|
||||||
|
downloadFileToTemp(sticker.fileId).asMultipartFile(),
|
||||||
|
sticker.stickerFormat,
|
||||||
|
listOf(sticker.emoji ?: "\uD83D\uDE0A"),
|
||||||
|
sticker.maskPosition
|
||||||
|
)
|
||||||
|
is RegularSticker -> InputSticker.WithKeywords.Regular(
|
||||||
|
downloadFileToTemp(sticker.fileId).asMultipartFile(),
|
||||||
|
sticker.stickerFormat,
|
||||||
|
listOf(sticker.emoji ?: "\uD83D\uDE0A"),
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
is UnknownSticker -> return@onSticker
|
||||||
|
}
|
||||||
|
runCatchingSafely {
|
||||||
|
getStickerSet(stickerSetName)
|
||||||
|
}.onSuccess { stickerSet ->
|
||||||
|
runCatching {
|
||||||
|
addStickerToSet(it.chat.id.toChatId(), stickerSet.name, newSticker).also { _ ->
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
getStickerSet(stickerSetName).stickers.last()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onFailure { exception ->
|
||||||
|
exception.printStackTrace()
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
"Unable to add sticker in stickerset"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onFailure { exception ->
|
||||||
|
createNewStickerSet(
|
||||||
|
it.chat.id.toChatId(),
|
||||||
|
stickerSetName.string,
|
||||||
|
"Sticker set by ${me.firstName}",
|
||||||
|
listOf(
|
||||||
|
newSticker
|
||||||
|
),
|
||||||
|
(sticker as? CustomEmojiSticker) ?.needsRepainting ?: false
|
||||||
|
).also { _ ->
|
||||||
|
reply(
|
||||||
|
it,
|
||||||
|
getStickerSet(stickerSetName).stickers.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
43
SuggestedPosts/README.md
Normal file
43
SuggestedPosts/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# SuggestedPosts
|
||||||
|
|
||||||
|
A bot that handles the channel Direct Messages (suggested post) approval flow.
|
||||||
|
|
||||||
|
## Functionality
|
||||||
|
|
||||||
|
Monitors suggested post events in a channel connected via Direct Messages. When a post is
|
||||||
|
suggested, the bot automatically schedules a decline after a short delay (demonstrating the
|
||||||
|
decline flow). Paid post events and approval/decline confirmations are also tracked and logged.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
| Position | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||||
|
|
||||||
|
Optional arguments (any order after the token):
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `debug` | Enable verbose debug logging |
|
||||||
|
| `testServer` | Connect to the Telegram test server instead of production |
|
||||||
|
|
||||||
|
## Bot Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/start` | Initialises the bot and confirms it is running |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Handles `SuggestedPostApproved` events
|
||||||
|
- Handles `SuggestedPostDeclined` events
|
||||||
|
- Handles `SuggestedPostPaid` and `SuggestedPostRefunded` events
|
||||||
|
- Handles `SuggestedPostApprovalFailed` errors
|
||||||
|
- Automatically declines new suggestions after a configurable delay to demonstrate the decline API
|
||||||
|
- Runs via long polling
|
||||||
|
|
||||||
|
## Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../gradlew run --args="BOT_TOKEN"
|
||||||
|
```
|
||||||
21
SuggestedPosts/build.gradle
Normal file
21
SuggestedPosts/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
mainClassName="SuggestedPostsBotKt"
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||||
|
}
|
||||||
140
SuggestedPosts/src/main/kotlin/SuggestedPostsBot.kt
Normal file
140
SuggestedPosts/src/main/kotlin/SuggestedPostsBot.kt
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.resend
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.suggested.approveSuggestedPost
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.suggested.declineSuggestedPost
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostApproved
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostDeclined
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChannelDirectMessagesConfigurationChanged
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostApprovalFailed
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostApproved
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostDeclined
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostPaid
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostRefunded
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.channelDirectMessagesContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.ChannelPaidPost
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||||
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
|
import dev.inmo.tgbotapi.utils.firstOf
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This place can be the playground for your code.
|
||||||
|
*/
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.Default),
|
||||||
|
testServer = isTestServer,
|
||||||
|
) {
|
||||||
|
// start here!!
|
||||||
|
val me = getMe()
|
||||||
|
println(me)
|
||||||
|
|
||||||
|
onCommand("start") {
|
||||||
|
println(getChat(it.chat))
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentMessage {
|
||||||
|
val message = it.channelDirectMessagesContentMessageOrNull() ?: return@onContentMessage
|
||||||
|
val chat = getChat(it.chat)
|
||||||
|
println(chat)
|
||||||
|
|
||||||
|
resend(
|
||||||
|
message.chat.id,
|
||||||
|
message.content,
|
||||||
|
suggestedPostParameters = SuggestedPostParameters()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentMessage(
|
||||||
|
subcontextUpdatesFilter = { _, _ -> true } // important to not miss updates in channel for waitSuggestedPost events
|
||||||
|
) { message ->
|
||||||
|
val suggestedPost = message.suggestedChannelDirectMessagesContentMessageOrNull() ?: return@onContentMessage
|
||||||
|
|
||||||
|
firstOf(
|
||||||
|
{
|
||||||
|
waitSuggestedPostApproved().filter {
|
||||||
|
it.suggestedPostMessage ?.chat ?.id == message.chat.id
|
||||||
|
}.first()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
waitSuggestedPostDeclined().filter {
|
||||||
|
it.suggestedPostMessage ?.chat ?.id == message.chat.id
|
||||||
|
}.first()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
for (i in 0 until 3) {
|
||||||
|
delay(1000L)
|
||||||
|
send(suggestedPost.chat, "${3 - i}")
|
||||||
|
}
|
||||||
|
declineSuggestedPost(suggestedPost)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onContentMessage(initialFilter = { it is ChannelPaidPost<*> }) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestedPostPaid {
|
||||||
|
println(it)
|
||||||
|
reply(it, "Paid")
|
||||||
|
}
|
||||||
|
onSuggestedPostApproved {
|
||||||
|
println(it)
|
||||||
|
reply(it, "Approved")
|
||||||
|
}
|
||||||
|
onSuggestedPostDeclined {
|
||||||
|
println(it)
|
||||||
|
reply(it, "Declined")
|
||||||
|
}
|
||||||
|
onSuggestedPostRefunded {
|
||||||
|
println(it)
|
||||||
|
reply(it, "Refunded")
|
||||||
|
}
|
||||||
|
onSuggestedPostApprovalFailed {
|
||||||
|
println(it)
|
||||||
|
reply(it, "Approval failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user