mirror of
https://github.com/fcitx5-android/fcitx5-android.git
synced 2026-08-02 04:44:35 +08:00
Merge de8439572f8f8fcbfeb823a4fa8bb332b3f7f029 into bb9cdbdbe9059b1eb9ea8581e2abd24e7ab1ca20
This commit is contained in:
commit
89add1542e
3
.gitignore
vendored
3
.gitignore
vendored
@ -102,3 +102,6 @@ lint/tmp/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
@ -23,6 +23,7 @@ import org.fcitx.fcitx5.android.core.reloadPinyinDict
|
||||
import org.fcitx.fcitx5.android.core.reloadQuickPhrase
|
||||
import org.fcitx.fcitx5.android.daemon.FcitxDaemon
|
||||
import org.fcitx.fcitx5.android.data.clipboard.ClipboardManager
|
||||
import org.fcitx.fcitx5.android.data.otp.OtpManager
|
||||
import org.fcitx.fcitx5.android.utils.Const
|
||||
import org.fcitx.fcitx5.android.utils.desc
|
||||
import org.fcitx.fcitx5.android.utils.descEquals
|
||||
@ -93,8 +94,8 @@ class FcitxRemoteService : Service() {
|
||||
Timber.d("unregisterClipboardEntryTransformer: ${transformer.desc}")
|
||||
scope.launch {
|
||||
clipboardTransformers.remove(transformer)
|
||||
|| clipboardTransformers.removeAll { it.descEquals(transformer) }
|
||||
|| return@launch
|
||||
|| clipboardTransformers.removeAll { it.descEquals(transformer) }
|
||||
|| return@launch
|
||||
updateClipboardManager()
|
||||
}
|
||||
}
|
||||
@ -106,6 +107,13 @@ class FcitxRemoteService : Service() {
|
||||
override fun reloadQuickPhrase() {
|
||||
FcitxDaemon.getFirstConnectionOrNull()?.runIfReady { reloadQuickPhrase() }
|
||||
}
|
||||
|
||||
override fun updateOtp(otp: String?) {
|
||||
Timber.d("updateOtp called: $otp")
|
||||
otp?.let {
|
||||
OtpManager.updateOtp(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.data.otp
|
||||
|
||||
import org.fcitx.fcitx5.android.utils.WeakHashSet
|
||||
|
||||
object OtpManager {
|
||||
fun interface OnOtpReceivedListener {
|
||||
fun onOtpReceived(otp: String)
|
||||
}
|
||||
|
||||
private val listeners = WeakHashSet<OnOtpReceivedListener>()
|
||||
|
||||
fun addOnOtpReceivedListener(listener: OnOtpReceivedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeOnOtpReceivedListener(listener: OnOtpReceivedListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun updateOtp(otp: String) {
|
||||
listeners.forEach { it.onOtpReceived(otp) }
|
||||
}
|
||||
}
|
||||
@ -82,12 +82,15 @@ import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.min
|
||||
import org.fcitx.fcitx5.android.data.otp.OtpManager
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(),
|
||||
InputBroadcastReceiver {
|
||||
|
||||
private var otpCode: String? = null
|
||||
|
||||
private val context by manager.context()
|
||||
private val theme by manager.theme()
|
||||
private val service by manager.inputMethodService()
|
||||
@ -108,12 +111,14 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
|
||||
private val preferredVoiceInput by prefs.keyboard.preferredVoiceInput
|
||||
|
||||
private var clipboardTimeoutJob: Job? = null
|
||||
private var otpCodeTimeoutJob: Job? = null
|
||||
|
||||
private var isClipboardFresh: Boolean = false
|
||||
private var isInlineSuggestionPresent: Boolean = false
|
||||
private var isCapabilityFlagsPassword: Boolean = false
|
||||
private var isKeyboardLayoutNumber: Boolean = false
|
||||
private var isToolbarManuallyToggled: Boolean = false
|
||||
private var isOtpCodeFresh: Boolean = false
|
||||
|
||||
private enum class NumberRowState { Auto, ForceShow, ForceHide }
|
||||
|
||||
@ -174,8 +179,33 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
|
||||
}
|
||||
}
|
||||
|
||||
private val otpListener = OtpManager.OnOtpReceivedListener { otp ->
|
||||
if (idleUi.otpUi.text.text == null) return@OnOtpReceivedListener
|
||||
service.lifecycleScope.launch {
|
||||
idleUi.otpUi.text.text = otp
|
||||
otpCode = otp
|
||||
isOtpCodeFresh = true
|
||||
launchOtpCodeTimeoutJob()
|
||||
evalIdleUiState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchOtpCodeTimeoutJob() {
|
||||
otpCodeTimeoutJob?.cancel()
|
||||
val timeout = clipboardItemTimeout.getValue() * 1000L
|
||||
// never transition to ClipboardTimedOut state when timeout < 0
|
||||
if (timeout < 0L) return
|
||||
otpCodeTimeoutJob = service.lifecycleScope.launch {
|
||||
delay(timeout)
|
||||
otpCode = null
|
||||
isOtpCodeFresh = false
|
||||
otpCodeTimeoutJob = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun evalIdleUiState(fromUser: Boolean = false) {
|
||||
val newState = when {
|
||||
isOtpCodeFresh -> IdleUi.State.Otp
|
||||
numberRowState == NumberRowState.ForceShow -> IdleUi.State.NumberRow
|
||||
isClipboardFresh -> IdleUi.State.Clipboard
|
||||
isInlineSuggestionPresent -> IdleUi.State.InlineSuggestion
|
||||
@ -334,6 +364,11 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
|
||||
true
|
||||
}
|
||||
}
|
||||
otpUi.suggestionView.setOnClickListener {
|
||||
otpCode?.let(service::commitText)
|
||||
otpCode = null
|
||||
isOtpCodeFresh = false
|
||||
evalIdleUiState()
|
||||
numberRow.apply {
|
||||
onCollapseListener = {
|
||||
numberRowState = NumberRowState.ForceHide
|
||||
@ -439,6 +474,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
|
||||
ClipboardManager.addOnUpdateListener(onClipboardUpdateListener)
|
||||
clipboardSuggestion.registerOnChangeListener(onClipboardSuggestionUpdateListener)
|
||||
clipboardItemTimeout.registerOnChangeListener(onClipboardTimeoutUpdateListener)
|
||||
OtpManager.addOnOtpReceivedListener(otpListener)
|
||||
}
|
||||
|
||||
override fun onStartInput(info: EditorInfo, capFlags: CapabilityFlags) {
|
||||
|
||||
@ -50,7 +50,7 @@ class IdleUi(
|
||||
) : Ui {
|
||||
|
||||
enum class State {
|
||||
Empty, Toolbar, Clipboard, NumberRow, InlineSuggestion
|
||||
Empty, Toolbar, Clipboard, NumberRow, InlineSuggestion, Otp
|
||||
}
|
||||
|
||||
var currentState = State.Empty
|
||||
@ -81,7 +81,7 @@ class IdleUi(
|
||||
|
||||
val buttonsUi = ButtonsBarUi(ctx, theme)
|
||||
|
||||
val clipboardUi = ClipboardSuggestionUi(ctx, theme)
|
||||
val clipboardUi = ClipboardSuggestionUi(ctx, theme, false)
|
||||
|
||||
val numberRow = NumberRow(ctx, theme).apply {
|
||||
visibility = View.GONE
|
||||
@ -89,11 +89,14 @@ class IdleUi(
|
||||
|
||||
val inlineSuggestionsBar = InlineSuggestionsUi(ctx)
|
||||
|
||||
val otpUi = ClipboardSuggestionUi(ctx, theme, true)
|
||||
|
||||
private val animator = ViewAnimator(ctx).apply {
|
||||
add(emptyBar, lParams(matchParent, matchParent))
|
||||
add(buttonsUi.root, lParams(matchParent, matchParent))
|
||||
add(clipboardUi.root, lParams(matchParent, matchParent))
|
||||
add(inlineSuggestionsBar.root, lParams(matchParent, matchParent))
|
||||
add(otpUi.root, lParams(matchParent, matchParent))
|
||||
}
|
||||
|
||||
private val inAnimation by lazy {
|
||||
@ -222,6 +225,7 @@ class IdleUi(
|
||||
State.Clipboard -> animator.displayedChild = 2
|
||||
State.NumberRow -> {}
|
||||
State.InlineSuggestion -> animator.displayedChild = 3
|
||||
State.Otp -> animator.displayedChild = 4
|
||||
}
|
||||
if (state == State.NumberRow) {
|
||||
numberRow.keyActionListener = commonKeyActionListener.listener
|
||||
|
||||
@ -31,11 +31,17 @@ import splitties.views.dsl.core.verticalMargin
|
||||
import splitties.views.dsl.core.wrapContent
|
||||
import splitties.views.imageDrawable
|
||||
|
||||
class ClipboardSuggestionUi(override val ctx: Context, private val theme: Theme) : Ui {
|
||||
class ClipboardSuggestionUi(override val ctx: Context, private val theme: Theme, private val isOtp: Boolean) : Ui {
|
||||
|
||||
private val icon = imageView {
|
||||
imageDrawable = drawable(R.drawable.ic_clipboard)!!.apply {
|
||||
setTint(theme.altKeyTextColor)
|
||||
imageDrawable = if (isOtp) {
|
||||
drawable(R.drawable.ic_baseline_library_books_24)!!.apply {
|
||||
setTint(theme.altKeyTextColor)
|
||||
}
|
||||
} else {
|
||||
drawable(R.drawable.ic_clipboard)!!.apply {
|
||||
setTint(theme.altKeyTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,4 +22,6 @@ interface IFcitxRemoteService {
|
||||
void reloadPinyinDict();
|
||||
/** Reload fcitx quick phrase */
|
||||
void reloadQuickPhrase();
|
||||
/** Update OTP from plugin */
|
||||
void updateOtp(in String Otp);
|
||||
}
|
||||
39
plugin/sms-otp/build.gradle.kts
Normal file
39
plugin/sms-otp/build.gradle.kts
Normal file
@ -0,0 +1,39 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
plugins {
|
||||
id("org.fcitx.fcitx5.android.app-convention")
|
||||
id("org.fcitx.fcitx5.android.plugin-app-convention")
|
||||
id("org.fcitx.fcitx5.android.build-metadata")
|
||||
id("org.fcitx.fcitx5.android.data-descriptor")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.fcitx.fcitx5.android.plugin.smsotp"
|
||||
defaultConfig {
|
||||
applicationId = "org.fcitx.fcitx5.android.plugin.smsotp"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
resValue("string", "app_name", "@string/app_name_release")
|
||||
}
|
||||
debug {
|
||||
resValue("string", "app_name", "@string/app_name_debug")
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
getByName("test") {
|
||||
java.srcDir("src/main/java")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:plugin-base"))
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:2.1.20")
|
||||
}
|
||||
|
||||
|
||||
0
plugin/sms-otp/licenses/.gitkeep
Normal file
0
plugin/sms-otp/licenses/.gitkeep
Normal file
25
plugin/sms-otp/src/main/AndroidManifest.xml
Normal file
25
plugin/sms-otp/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
|
||||
<uses-permission android:name="${mainApplicationId}.permission.IPC" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher_plugin_generic"
|
||||
android:label="@string/app_name">
|
||||
<service
|
||||
android:name=".MainService"
|
||||
android:directBootAware="true"
|
||||
android:exported="true"
|
||||
android:permission="${mainApplicationId}.permission.PLUGIN"
|
||||
tools:targetApi="26">
|
||||
<intent-filter>
|
||||
<action android:name="${mainApplicationId}.plugin.SERVICE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
@ -0,0 +1,106 @@
|
||||
package org.fcitx.fcitx5.android.plugin.smsotp
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.util.regex.Pattern
|
||||
import org.fcitx.fcitx5.android.common.FcitxPluginService
|
||||
import org.fcitx.fcitx5.android.common.ipc.FcitxRemoteConnection
|
||||
import org.fcitx.fcitx5.android.common.ipc.bindFcitxRemoteService
|
||||
|
||||
fun extractOtp(message: String?): String? {
|
||||
if (message == null) return null
|
||||
val pattern =
|
||||
Pattern.compile("""(?<!(\d|\d.|\d\s{1,4}|/))(\d{4,8}|\d{3}-\d{3})(?!(\d|\.\d|\s+\d|/|-))""")
|
||||
val matcher = pattern.matcher(message)
|
||||
val results = mutableListOf<String>()
|
||||
val codePositions = mutableListOf<Pair<String, Int>>()
|
||||
while (matcher.find()) {
|
||||
val code = matcher.group(2).replace("-", "")
|
||||
results.add(code)
|
||||
codePositions.add(Pair(code, matcher.start(2)))
|
||||
}
|
||||
if (results.size > 1) {
|
||||
val keywords = listOf("code", "otp", "码", "碼", "コード", "코드", "код", "kodo")
|
||||
val keywordPositions = keywords.flatMap { keyword ->
|
||||
Regex(Regex.escape(keyword), RegexOption.IGNORE_CASE)
|
||||
.findAll(message)
|
||||
.map { it.range.first }
|
||||
.toList()
|
||||
}
|
||||
if (keywordPositions.isNotEmpty()) {
|
||||
// find the nearest one
|
||||
val nearest = codePositions.minByOrNull { (code, pos) ->
|
||||
keywordPositions.minOf { kp -> kotlin.math.abs(pos - kp) }
|
||||
}
|
||||
return nearest?.first
|
||||
} else {
|
||||
return results.firstOrNull()
|
||||
}
|
||||
}
|
||||
else {
|
||||
return results.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
class MainService : FcitxPluginService() {
|
||||
|
||||
private lateinit var connection: FcitxRemoteConnection
|
||||
|
||||
override fun start() {
|
||||
registerSmsReceiver(this)
|
||||
connection =
|
||||
bindFcitxRemoteService(BuildConfig.MAIN_APPLICATION_ID) { log("Bind to fcitx remote") }
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
unregisterSmsReceiver(this)
|
||||
unbindService(connection)
|
||||
log("Unbind from fcitx remote")
|
||||
}
|
||||
private fun log(msg: String) {
|
||||
Log.d("SmsOtpService", msg)
|
||||
}
|
||||
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
private fun registerSmsReceiver(context: Context) {
|
||||
if (receiver != null) return
|
||||
receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val messages = android.provider.Telephony.Sms.Intents.getMessagesFromIntent(intent)
|
||||
val fullMessage = messages.joinToString("") { it.messageBody }
|
||||
val code = extractOtp(fullMessage)
|
||||
if (code != null) {
|
||||
log("OTP Detected: $code")
|
||||
tryInputOtp(context, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
context.registerReceiver(receiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterSmsReceiver(context: Context) {
|
||||
receiver?.let {
|
||||
context.unregisterReceiver(it)
|
||||
receiver = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryInputOtp(context: Context, code: String) {
|
||||
try {
|
||||
connection.remoteService?.updateOtp(code)
|
||||
} catch (e: Exception) {
|
||||
log("updateOtp failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
Read OTP from SMS message
|
||||
1
plugin/sms-otp/src/main/play/listings/en-US/title.txt
Normal file
1
plugin/sms-otp/src/main/play/listings/en-US/title.txt
Normal file
@ -0,0 +1 @@
|
||||
Fcitx5 (SMS OTP Plugin)
|
||||
@ -0,0 +1 @@
|
||||
自动获取短信验证码
|
||||
1
plugin/sms-otp/src/main/play/listings/zh-CN/title.txt
Normal file
1
plugin/sms-otp/src/main/play/listings/zh-CN/title.txt
Normal file
@ -0,0 +1 @@
|
||||
小企鹅输入法 (短信验证码插件)
|
||||
6
plugin/sms-otp/src/main/res/values-zh-rCN/strings.xml
Normal file
6
plugin/sms-otp/src/main/res/values-zh-rCN/strings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name_debug">小企鹅输入法 (短信验证码插件 | 调试)</string>
|
||||
<string name="app_name_release">小企鹅输入法 (短信验证码插件)</string>
|
||||
<string name="description">自动获取短信验证码</string>
|
||||
</resources>
|
||||
7
plugin/sms-otp/src/main/res/values/strings.xml
Normal file
7
plugin/sms-otp/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Fcitx5 (SMS OTP Plugin)</string>
|
||||
<string name="app_name_debug">Fcitx5 (SMS OTP Plugin | Debug)</string>
|
||||
<string name="app_name_release">Fcitx5 (SMS OTP Plugin)</string>
|
||||
<string name="description">Read OTP from SMS message</string>
|
||||
</resources>
|
||||
6
plugin/sms-otp/src/main/res/xml/plugin.xml
Normal file
6
plugin/sms-otp/src/main/res/xml/plugin.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<plugin xmlns="../../../../../pluginSchema.xsd">
|
||||
<apiVersion>0.1</apiVersion>
|
||||
<description>@string/description</description>
|
||||
<hasService>true</hasService>
|
||||
</plugin>
|
||||
@ -0,0 +1,55 @@
|
||||
package org.fcitx.fcitx5.android.plugin.smsotp
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class SmSFormatTest {
|
||||
@Test
|
||||
fun testExtractOtp_basic() {
|
||||
assertEquals("123456", extractOtp("Your OTP is 123456."))
|
||||
assertEquals("987654", extractOtp("验证码:987654"))
|
||||
assertEquals("1234", extractOtp("Code: 1234"))
|
||||
assertEquals("12345678", extractOtp("OTP: 12345678"))
|
||||
assertEquals("123456", extractOtp("123-456"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractOtp_withNoise() {
|
||||
assertEquals("123456", extractOtp("abc 123456 def"))
|
||||
assertNull(extractOtp("abc.1234567/def"))
|
||||
assertNull(extractOtp("2004/10/12, 12:00:00"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractOtp_invalid() {
|
||||
assertNull(extractOtp(null))
|
||||
assertNull(extractOtp("No code here"))
|
||||
assertNull(extractOtp("123")) // too short
|
||||
assertNull(extractOtp("123456789")) // too long
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractOtp_multipleCodes() {
|
||||
assertEquals("123456", extractOtp("First: 123456, Second: 654321"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtractOtp_realSamples() {
|
||||
assertEquals("401572", extractOtp("WhatsApp code 401-572"))
|
||||
assertEquals("28843", extractOtp("28843 e o codigo de confirmacao do Facebook de Pedro Davi #fb"))
|
||||
assertEquals("932033", extractOtp("Your DENT code is: 932033"))
|
||||
assertEquals("050475", extractOtp("Ваш код перевірки Poe: 050475. Не повідомляйте цей код іншим."))
|
||||
assertEquals("429309", extractOtp("[抖音] 429309 is your verification code, valid for 5 minutes."))
|
||||
assertEquals("8650", extractOtp("<#> 8650 is your Venmo phone verification code."))
|
||||
assertEquals("060973", extractOtp("12306用户注册或既有用户手机核验专用验证码:060973。如非本人直接访问12306,请停止操作,切勿将验证码提供给第三方。【铁路客服】"))
|
||||
assertEquals("505513", extractOtp("您在付款,为防诈骗千万不要告诉他人验证码505513,商户为汇付天下,金额80元。如有疑问请停止操作。(短信编号:245747)【工商银行】"))
|
||||
assertEquals("370000", extractOtp("任何向你索要验证码的都是骗子,千万别给!您正在向www(尾号4832)转账,验证码370000,100元。"))
|
||||
assertEquals("927711", extractOtp("TAN to confirm mandate for Segpay UK Test Please enter TAN 927711 to set up your mandate 10751532bd3573241262f56b4e76a003 securely."))
|
||||
assertEquals("121093", extractOtp("Twój kod do Tindera to 121093 Nie udostępniaj @tinder.com #121093"))
|
||||
assertNull(extractOtp("OKX:USDTsuccessfully withdrawn: 132,619.89 【okxcash.com】Account: DTm888 Key: Swksf367"))
|
||||
assertNull(extractOtp("You can also tap on this link to verify your phone: v.whatsapp.com/696948"))
|
||||
assertNull(extractOtp("https://www.photosfromyourevent.com/4669/wauem8/"))
|
||||
}
|
||||
|
||||
}
|
||||
@ -35,3 +35,4 @@ include(":plugin:chewing")
|
||||
include(":plugin:sayura")
|
||||
include(":plugin:jyutping")
|
||||
include(":plugin:thai")
|
||||
include(":plugin:sms-otp")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user