mirror of
				https://github.com/InsanusMokrassar/TelegramBotApiLibraries.git
				synced 2025-11-04 14:15:44 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			0.18.1
			...
			feature/fs
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b45bd3192a | |||
| c4e18ad25f | |||
| 6f17a53146 | |||
| f04f065ac5 | 
							
								
								
									
										17
									
								
								fsm/core/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								fsm/core/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					plugins {
 | 
				
			||||||
 | 
					    id "org.jetbrains.kotlin.multiplatform"
 | 
				
			||||||
 | 
					    id "org.jetbrains.kotlin.plugin.serialization"
 | 
				
			||||||
 | 
					    id "com.android.library"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$mppProjectWithSerializationPresetPath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					kotlin {
 | 
				
			||||||
 | 
					    sourceSets {
 | 
				
			||||||
 | 
					        commonMain {
 | 
				
			||||||
 | 
					            dependencies {
 | 
				
			||||||
 | 
					                api "dev.inmo:micro_utils.coroutines:$micro_utils_version"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed interface State {
 | 
				
			||||||
 | 
					    val context: Any
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Use this state as parent of your state in case you want to avoid saving of this state in queue for [context] if this
 | 
				
			||||||
 | 
					 * queue is not empty
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					interface ImmediateOrNeverState : State
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Use this state as parent of your state in case you want to keep saving of this state in queue for [context] if this
 | 
				
			||||||
 | 
					 * queue is not empty
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					interface QueueableState : State
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlin.reflect.KClass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StateHandlerHolder<I : State>(
 | 
				
			||||||
 | 
					    private val inputKlass: KClass<I>,
 | 
				
			||||||
 | 
					    private val strict: Boolean = false,
 | 
				
			||||||
 | 
					    private val delegateTo: StatesHandler<I>
 | 
				
			||||||
 | 
					) : StatesHandler<State> {
 | 
				
			||||||
 | 
					    fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override suspend fun StatesMachine.handleState(state: State): State? {
 | 
				
			||||||
 | 
					        return delegateTo.run { handleState(state as I) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun interface StatesHandler<I : State> {
 | 
				
			||||||
 | 
					    suspend fun StatesMachine.handleState(state: I): State?
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import dev.inmo.micro_utils.coroutines.*
 | 
				
			||||||
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.asFlow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private suspend fun <I : State> StatesMachine.launchStateHandling(
 | 
				
			||||||
 | 
					    state: State,
 | 
				
			||||||
 | 
					    handlers: List<StateHandlerHolder<out I>>
 | 
				
			||||||
 | 
					): State? {
 | 
				
			||||||
 | 
					    return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
 | 
				
			||||||
 | 
					        handleState(state)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StatesMachine (
 | 
				
			||||||
 | 
					    private val statesManager: StatesManager,
 | 
				
			||||||
 | 
					    private val handlers: List<StateHandlerHolder<*>>
 | 
				
			||||||
 | 
					) : StatesHandler<State> {
 | 
				
			||||||
 | 
					    override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
 | 
				
			||||||
 | 
					        val statePerformer: suspend (State) -> Unit = { state: State ->
 | 
				
			||||||
 | 
					            val newState = launchStateHandling(state, handlers)
 | 
				
			||||||
 | 
					            if (newState != null) {
 | 
				
			||||||
 | 
					                statesManager.update(state, newState)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                statesManager.endChain(state)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
 | 
				
			||||||
 | 
					            launch { statePerformer(it) }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
 | 
				
			||||||
 | 
					            launch { statePerformer(it.second) }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        statesManager.getActiveStates().forEach {
 | 
				
			||||||
 | 
					            launch { statePerformer(it) }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suspend fun startChain(state: State) {
 | 
				
			||||||
 | 
					        statesManager.startChain(state)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.coroutines.flow.*
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.Mutex
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.withLock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface StatesManager {
 | 
				
			||||||
 | 
					    val onChainStateUpdated: Flow<Pair<State, State>>
 | 
				
			||||||
 | 
					    val onStartChain: Flow<State>
 | 
				
			||||||
 | 
					    val onEndChain: Flow<State>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Must set current set using [State.context]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    suspend fun update(old: State, new: State)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
 | 
				
			||||||
 | 
					     * busy by the other [State]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    suspend fun startChain(state: State)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
 | 
				
			||||||
 | 
					     * ignored
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    suspend fun endChain(state: State)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suspend fun getActiveStates(): List<State>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
 | 
				
			||||||
 | 
					 * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
 | 
				
			||||||
 | 
					 * new state by using [endChain] with that state
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class InMemoryStatesManager(
 | 
				
			||||||
 | 
					    private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
 | 
				
			||||||
 | 
					) : StatesManager {
 | 
				
			||||||
 | 
					    private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
 | 
				
			||||||
 | 
					    override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
 | 
				
			||||||
 | 
					    private val _onStartChain = MutableSharedFlow<State>(0)
 | 
				
			||||||
 | 
					    override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
 | 
				
			||||||
 | 
					    private val _onEndChain = MutableSharedFlow<State>(0)
 | 
				
			||||||
 | 
					    override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val contextsToStates = mutableMapOf<Any, State>()
 | 
				
			||||||
 | 
					    private val mapMutex = Mutex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override suspend fun update(old: State, new: State) = mapMutex.withLock {
 | 
				
			||||||
 | 
					        when {
 | 
				
			||||||
 | 
					            contextsToStates[old.context] != old -> return@withLock
 | 
				
			||||||
 | 
					            old.context == new.context || !contextsToStates.containsKey(new.context) -> {
 | 
				
			||||||
 | 
					                contextsToStates[old.context] = new
 | 
				
			||||||
 | 
					                _onChainStateUpdated.emit(old to new)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else -> {
 | 
				
			||||||
 | 
					                val stateOnNewOneContext = contextsToStates.getValue(new.context)
 | 
				
			||||||
 | 
					                if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
 | 
				
			||||||
 | 
					                    endChainWithoutLock(stateOnNewOneContext)
 | 
				
			||||||
 | 
					                    contextsToStates.remove(old.context)
 | 
				
			||||||
 | 
					                    contextsToStates[new.context] = new
 | 
				
			||||||
 | 
					                    _onChainStateUpdated.emit(old to new)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override suspend fun startChain(state: State) = mapMutex.withLock {
 | 
				
			||||||
 | 
					        if (!contextsToStates.containsKey(state.context)) {
 | 
				
			||||||
 | 
					            contextsToStates[state.context] = state
 | 
				
			||||||
 | 
					            _onStartChain.emit(state)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun endChainWithoutLock(state: State) {
 | 
				
			||||||
 | 
					        if (contextsToStates[state.context] == state) {
 | 
				
			||||||
 | 
					            contextsToStates.remove(state.context)
 | 
				
			||||||
 | 
					            _onEndChain.emit(state)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override suspend fun endChain(state: State) {
 | 
				
			||||||
 | 
					        mapMutex.withLock {
 | 
				
			||||||
 | 
					            endChainWithoutLock(state)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					package dev.inmo.tgbotapi.libraries.fsm.core.dsl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import dev.inmo.tgbotapi.libraries.fsm.core.*
 | 
				
			||||||
 | 
					import kotlin.reflect.KClass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FSMBuilder(
 | 
				
			||||||
 | 
					    var statesManager: StatesManager = InMemoryStatesManager()
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    private var states = mutableListOf<StateHandlerHolder<*>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
 | 
				
			||||||
 | 
					        states.add(StateHandlerHolder(kClass, false, handler))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
 | 
				
			||||||
 | 
					        states.add(StateHandlerHolder(kClass, true, handler))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun build() = StatesMachine(
 | 
				
			||||||
 | 
					        statesManager,
 | 
				
			||||||
 | 
					        states.toList()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
 | 
				
			||||||
 | 
					    add(I::class, handler)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
 | 
				
			||||||
 | 
					    addStrict(I::class, handler)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun buildFSM(
 | 
				
			||||||
 | 
					    block: FSMBuilder.() -> Unit
 | 
				
			||||||
 | 
					): StatesMachine = FSMBuilder().apply(block).build()
 | 
				
			||||||
							
								
								
									
										54
									
								
								fsm/core/src/jvmTest/kotlin/PlayableMain.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								fsm/core/src/jvmTest/kotlin/PlayableMain.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					import dev.inmo.tgbotapi.libraries.fsm.core.*
 | 
				
			||||||
 | 
					import dev.inmo.tgbotapi.libraries.fsm.core.dsl.buildFSM
 | 
				
			||||||
 | 
					import dev.inmo.tgbotapi.libraries.fsm.core.dsl.strictlyOn
 | 
				
			||||||
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
 | 
					import kotlin.random.Random
 | 
				
			||||||
 | 
					import kotlin.test.Test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed interface TrafficLightState : ImmediateOrNeverState {
 | 
				
			||||||
 | 
					    val trafficLightNumber: Int
 | 
				
			||||||
 | 
					    override val context: Int
 | 
				
			||||||
 | 
					        get() = trafficLightNumber
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState
 | 
				
			||||||
 | 
					data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState
 | 
				
			||||||
 | 
					data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlayableMain {
 | 
				
			||||||
 | 
					    fun test() {
 | 
				
			||||||
 | 
					        runBlocking {
 | 
				
			||||||
 | 
					            val countOfTrafficLights = 10
 | 
				
			||||||
 | 
					            val initialStates = (0 until countOfTrafficLights).map {
 | 
				
			||||||
 | 
					                when (0/*Random.nextInt(3)*/) {
 | 
				
			||||||
 | 
					                    0 -> GreenCommon(it)
 | 
				
			||||||
 | 
					                    1 -> YellowCommon(it)
 | 
				
			||||||
 | 
					                    else -> RedCommon(it)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val statesManager = InMemoryStatesManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val machine = buildFSM {
 | 
				
			||||||
 | 
					                strictlyOn<GreenCommon> {
 | 
				
			||||||
 | 
					                    delay(1000L)
 | 
				
			||||||
 | 
					                    YellowCommon(it.context).also(::println)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                strictlyOn<YellowCommon> {
 | 
				
			||||||
 | 
					                    delay(1000L)
 | 
				
			||||||
 | 
					                    RedCommon(it.context).also(::println)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                strictlyOn<RedCommon> {
 | 
				
			||||||
 | 
					                    delay(1000L)
 | 
				
			||||||
 | 
					                    GreenCommon(it.context).also(::println)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.statesManager = statesManager
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            initialStates.forEach { machine.startChain(it) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val scope = CoroutineScope(Dispatchers.Default)
 | 
				
			||||||
 | 
					            machine.start(scope).join()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								fsm/core/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fsm/core/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<manifest package="dev.inmo.tgbotapi.libraries.fsm.core"/>
 | 
				
			||||||
							
								
								
									
										17
									
								
								fsm/tgbotapi/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								fsm/tgbotapi/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					plugins {
 | 
				
			||||||
 | 
					    id "org.jetbrains.kotlin.multiplatform"
 | 
				
			||||||
 | 
					    id "org.jetbrains.kotlin.plugin.serialization"
 | 
				
			||||||
 | 
					    id "com.android.library"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$mppProjectWithSerializationPresetPath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					kotlin {
 | 
				
			||||||
 | 
					    sourceSets {
 | 
				
			||||||
 | 
					        commonMain {
 | 
				
			||||||
 | 
					            dependencies {
 | 
				
			||||||
 | 
					                api project(":tgbotapi.libraries.fsm.core")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								fsm/tgbotapi/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								fsm/tgbotapi/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<manifest package="dev.inmo.tgbotapi.libraries.fsm.tgbotapi"/>
 | 
				
			||||||
@@ -8,6 +8,7 @@ android.enableJetifier=true
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
kotlin_version=1.5.10
 | 
					kotlin_version=1.5.10
 | 
				
			||||||
kotlin_serialisation_core_version=1.2.1
 | 
					kotlin_serialisation_core_version=1.2.1
 | 
				
			||||||
 | 
					kotlin_coroutines_version=1.5.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
github_release_plugin_version=2.2.12
 | 
					github_release_plugin_version=2.2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,10 @@ String[] includes = [
 | 
				
			|||||||
    ":cache:admins:common",
 | 
					    ":cache:admins:common",
 | 
				
			||||||
    ":cache:admins:micro_utils",
 | 
					    ":cache:admins:micro_utils",
 | 
				
			||||||
    ":cache:admins:plagubot",
 | 
					    ":cache:admins:plagubot",
 | 
				
			||||||
    ":cache:media"
 | 
					    ":cache:media",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ":fsm:core",
 | 
				
			||||||
 | 
					    ":fsm:tgbotapi"
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user