From c9bae32c33c51aa152a5b6943f164a261798cb68 Mon Sep 17 00:00:00 2001 From: Chenhe Date: Sun, 12 Nov 2023 20:31:16 +0000 Subject: [PATCH] adapt to tim 3.5.5.3198 --- .../core/DelegateNotificationResolver.kt | 26 +++ .../qqnotifyevo/core/NotificationProcessor.kt | 4 +- .../core/TimNotificationResolver.kt | 160 ++++++++++++++++++ .../core/TimNotificationResolverTest.kt | 132 +++++++++++++++ 4 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/cc/chenhe/qqnotifyevo/core/DelegateNotificationResolver.kt create mode 100644 app/src/main/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolver.kt create mode 100644 app/src/test/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolverTest.kt diff --git a/app/src/main/java/cc/chenhe/qqnotifyevo/core/DelegateNotificationResolver.kt b/app/src/main/java/cc/chenhe/qqnotifyevo/core/DelegateNotificationResolver.kt new file mode 100644 index 0000000..5c88375 --- /dev/null +++ b/app/src/main/java/cc/chenhe/qqnotifyevo/core/DelegateNotificationResolver.kt @@ -0,0 +1,26 @@ +package cc.chenhe.qqnotifyevo.core + +import cc.chenhe.qqnotifyevo.utils.Tag + +/** + * A [NotificationResolver] that call different implementations based on [Tag]. + */ +class DelegateNotificationResolver : NotificationResolver { + private val qqResolver by lazy { QQNotificationResolver() } + private val timResolver by lazy { TimNotificationResolver() } + + override fun resolveNotification( + tag: Tag, + title: String?, + content: String?, + ticker: String? + ): QQNotification? { + return when (tag) { + Tag.UNKNOWN -> null + Tag.QQ -> qqResolver + Tag.QQ_HD -> qqResolver + Tag.QQ_LITE -> qqResolver + Tag.TIM -> timResolver + }?.run { resolveNotification(tag, title, content, ticker) } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/chenhe/qqnotifyevo/core/NotificationProcessor.kt b/app/src/main/java/cc/chenhe/qqnotifyevo/core/NotificationProcessor.kt index 54c6420..f6e19aa 100644 --- a/app/src/main/java/cc/chenhe/qqnotifyevo/core/NotificationProcessor.kt +++ b/app/src/main/java/cc/chenhe/qqnotifyevo/core/NotificationProcessor.kt @@ -68,6 +68,7 @@ abstract class NotificationProcessor(context: Context, scope: CoroutineScope) { private val avatarManager = AvatarManager.get(getAvatarDiskCacheDir(ctx), getAvatarCachePeriod(context)) + private val resolver: NotificationResolver = DelegateNotificationResolver() init { scope.launch { @@ -169,12 +170,9 @@ abstract class NotificationProcessor(context: Context, scope: CoroutineScope) { val original = sbn.notification ?: return null val tag = getTagFromPackageName(packageName) if (tag == Tag.UNKNOWN) { - Timber.tag(TAG).d("Unknown tag, skip. pkgName=%s", packageName) return null } - val resolver: NotificationResolver = QQNotificationResolver() - return when (val r = resolver.resolveNotification(packageName, tag, sbn)) { is QQNotification.BindingAccountMessage -> { val conversation = addMessage( diff --git a/app/src/main/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolver.kt b/app/src/main/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolver.kt new file mode 100644 index 0000000..5f3e9ab --- /dev/null +++ b/app/src/main/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolver.kt @@ -0,0 +1,160 @@ +package cc.chenhe.qqnotifyevo.core + +import cc.chenhe.qqnotifyevo.utils.Tag +import timber.log.Timber + +/** + * For com.tencent.tim ver 3.5.5.3198 build 1328. + * + * Doesn't support QZone notifications because TIM failed to post anything about QZone. + */ +class TimNotificationResolver : NotificationResolver { + companion object { + private const val TAG = "TimNotificationResolver" + + // 隐藏消息详情 + // title: TIM + // ticker: 你收到了x条新消息 + // text: 你收到了x条新消息 + + private const val HIDE_MESSAGE_TITLE = "TIM" + private val hideMsgTickerPattern = """^你收到了(?\d+)条新消息$""".toRegex() + + // 群聊消息 + // ------------- 单个消息 + // title: 群名 + // ticker: 昵称(群名):消息内容 + // text: [有关注的内容]昵称: 消息内容 + // ------------- 多个消息 + // title: 群名 (x条新消息) + // ticker: 昵称(群名):消息内容 + // text: [有关注的内容]昵称: 消息内容 + + /** + * 匹配群聊消息 Ticker. + * + * 限制:昵称不能包含英文括号 `()`. + */ + private val groupMsgPattern = + """^(?.+?)\((?.+?)\):(?[\s\S]+)$""".toRegex() + + private val groupTitlePattern = + """^(?.+?)(?: \((?\d+)条新消息\))?$""".toRegex() + + /** + * 匹配群聊消息 Content. + */ + private val groupMsgContentPattern = + """^(?\[有关注的内容])?(?.+?): (?[\s\S]+)$""".toRegex() + + // 私聊消息 + // title: [特别关心]昵称 | [特别关心]昵称 (x条新消息) + // ticker: 昵称: 消息内容 + // text: 消息内容 + + private val privateTitlePattern = + """^(?\[特别关心])?(?.+?)(?: \((?\d+)条新消息\))?$""".toRegex() + + + // 关联QQ消息 + // title: 关联QQ号 | 关联QQ号 (x条新消息) + // ticker: 关联QQ号-Sender:消息内容 + // text: Sender:消息内容 + + private val bindingTitlePattern = + """^关联QQ号(?: \((?\d+)条新消息\))?$""".toRegex() + + private val bindingTextPattern = + """^(?.+?):(?[\s\S]+)$""".toRegex() + } + + override fun resolveNotification( + tag: Tag, + title: String?, + content: String?, + ticker: String? + ): QQNotification? { + if (title.isNullOrEmpty() || content.isNullOrEmpty()) { + return null + } + if (isHidden(title = title, ticker = ticker)) { + return QQNotification.HiddenMessage(tag) + } + + if (ticker == null) { + Timber.tag(TAG).i("Ticker is null, skip") + return null + } + + tryResolveBindingMsg(tag, title, content)?.also { return it } + tryResolveGroupMsg(tag, title, content, ticker)?.also { return it } + tryResolvePrivateMsg(tag, title, content)?.also { return it } + + return null + } + + private fun isHidden(title: String?, ticker: String?): Boolean { + return title == HIDE_MESSAGE_TITLE && ticker != null + && hideMsgTickerPattern.matchEntire(ticker) != null + } + + private fun tryResolveGroupMsg( + tag: Tag, + title: String, + content: String, + ticker: String, + ): QQNotification? { + if (content.isEmpty() || ticker.isEmpty()) { + return null + } + val tickerGroups = groupMsgPattern.matchEntire(ticker)?.groups ?: return null + val titleGroups = groupTitlePattern.matchEntire(title)?.groups ?: return null + val contentGroups = groupMsgContentPattern.matchEntire(content)?.groups ?: return null + val name = tickerGroups["nickname"]?.value ?: return null + val groupName = titleGroups["group"]?.value ?: return null + val text = contentGroups["msg"]?.value ?: return null + val special = contentGroups["sp"]?.value != null + val num = titleGroups["num"]?.value?.toIntOrNull() + + return QQNotification.GroupMessage( + tag = tag, + groupName = groupName, + nickname = name, + message = text, + special = special, + num = num ?: 1, + ) + } + + private fun tryResolvePrivateMsg(tag: Tag, title: String, content: String): QQNotification? { + if (title.isEmpty() || content.isEmpty()) { + return null + } + val titleGroups = privateTitlePattern.matchEntire(title)?.groups ?: return null + val special = titleGroups["sp"] != null + val name = titleGroups["nickname"]?.value ?: return null + val num = titleGroups["num"]?.value?.toIntOrNull() + + return QQNotification.PrivateMessage( + tag = tag, + nickname = name, + message = content, + special = special, + num = num ?: 1, + ) + } + + private fun tryResolveBindingMsg( + tag: Tag, + title: String, + content: String + ): QQNotification? { + val titleGroups = bindingTitlePattern.matchEntire(title)?.groups ?: return null + val textGroups = bindingTextPattern.matchEntire(content)?.groups ?: return null + + val sender = textGroups["nickname"]?.value ?: return null + val text = textGroups["msg"]?.value ?: return null + val num = titleGroups["num"]?.value?.toIntOrNull() + return QQNotification.BindingAccountMessage(tag, sender, text, num ?: 1) + } +} \ No newline at end of file diff --git a/app/src/test/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolverTest.kt b/app/src/test/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolverTest.kt new file mode 100644 index 0000000..65bea68 --- /dev/null +++ b/app/src/test/java/cc/chenhe/qqnotifyevo/core/TimNotificationResolverTest.kt @@ -0,0 +1,132 @@ +package cc.chenhe.qqnotifyevo.core + +import cc.chenhe.qqnotifyevo.utils.Tag +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.types.shouldBeTypeOf +import org.junit.Before +import org.junit.Test + +class TimNotificationResolverTest : BaseResolverTest() { + private lateinit var resolver: TimNotificationResolver + + @Before + fun setup() { + resolver = TimNotificationResolver() + } + + private fun resolve(data: NotificationData): QQNotification? { + return resolver.resolveNotification( + tag = Tag.TIM, + title = data.title, + content = data.content, + ticker = data.ticker, + ) + } + + // 私聊消息 -––––--––––---––––---––––---––––---––––---–––– + + @Test + fun private_normal() { + val n = parse("""{"title":"咕咕咕","ticker":"咕咕咕: Hi","content":"Hi"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual(n.content!!) + r.num.shouldBeEqual(1) + r.special.shouldBeFalse() + } + + @Test + fun private_special() { + val n = + parse("""{"title":"[特别关心]咕咕咕","ticker":"咕咕咕: In memory of the days with another developer cs\nAnd I’m sorry ","content":"In memory of the days with another developer cs\nAnd I’m sorry "}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual(n.content!!) + r.num.shouldBeEqual(1) + r.special.shouldBeTrue() + } + + @Test + fun private_special_MultiMessage() { + val n = + parse("""{"title":"[特别关心]咕咕咕 (2条新消息)","ticker":"咕咕咕: &¥","content":"&¥"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual(n.content!!) + r.num.shouldBeEqual(2) + r.special.shouldBeTrue() + } + + // 群聊消息 -––––--––––---––––---––––---––––---––––---–––– + + @Test + fun group_normal() { + val n = + parse("""{"title":"测试群","ticker":"咕咕咕(测试群):Xxx","content":"咕咕咕: Xxx"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.groupName.shouldBeEqual("测试群") + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual("Xxx") + r.num.shouldBeEqual(1) + r.special.shouldBeFalse() + } + + @Test + fun group_multiMessage() { + val n = + parse("""{"title":"测试群 (2条新消息)","ticker":"咕咕咕(测试群):Yyy","content":"咕咕咕: Yyy"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.groupName.shouldBeEqual("测试群") + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual("Yyy") + r.num.shouldBeEqual(2) + r.special.shouldBeFalse() + } + + @Test + fun group_special() { + val n = + parse("""{"title":"测试群","ticker":"咕咕咕(测试群):111","content":"[有关注的内容]咕咕咕: 111"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.groupName.shouldBeEqual("测试群") + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual("111") + r.num.shouldBeEqual(1) + r.special.shouldBeTrue() + } + + @Test + fun group_special_multiMessage() { + val n = + parse("""{"title":"测试群 (2条新消息)","ticker":"咕咕咕(测试群):222","content":"[有关注的内容]咕咕咕: 222"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.groupName.shouldBeEqual("测试群") + r.nickname.shouldBeEqual("咕咕咕") + r.message.shouldBeEqual("222") + r.num.shouldBeEqual(2) + r.special.shouldBeTrue() + } + + // 其他 -––––--––––---––––---––––---––––---––––---–––– + + @Test + fun hidden() { + val n = + parse("""{"title":"TIM","ticker":"你收到了1条新消息","content":"你收到了1条新消息"}""") + resolve(n).shouldNotBeNull().shouldBeTypeOf() + } + + + @Test + fun binding_multiMessage_multiLine() { + val n = + parse("""{"title":"关联QQ号 (2条新消息)","ticker":"关联QQ号-\/dev\/urandom:a\nb","content":"\/dev\/urandom:a\nb"}""") + val r = resolve(n).shouldNotBeNull().shouldBeTypeOf() + r.sender.shouldBeEqual("/dev/urandom") + r.message.shouldBeEqual("a\nb") + r.num.shouldBeEqual(2) + } +} \ No newline at end of file