在 iOS 中使用 IdentifyLookup 进行短信过滤

3,154 阅读7分钟

垃圾短信是一个长期存在、令人困扰的问题。本文将介绍如何阻止这些短信、设备端的检测以及整合动态的服务器检测等。

Apple 在 WWDC 2017(iOS 11) 推出了 IdentityLookup 框架,让开发者可以参与到过滤短信的过程中。在 iOS 14,Apple 新增了两种过滤类别:交易信息(Promotion)、推广信息(transaction)。在 WWDC 2022(iOS 16),针对这两种类别,Apple 新增了 12 种子类别,推广信息包括 9 种子类别:其他(Others)、财务(Finance)、订单(Orders)、提醒(Reminders)、健康(Health)、天气(Weather)、运营商(Carrier)、奖励(Rewards)、公共服务(PublicServices)。交易信息包括 3 种子类别:其他(Others)、优惠(Offers)、优惠券(Coupons)。

IdentifyLookup.png

消息过滤流程

消息过滤通过应用程序扩展(App extension)来完成。当用户收到来自未知发件人的消息时,“消息” APP 通过询问 Message Filter Extension,来确定该消息的类别。Message Filter Extension 可以通过使用内置逻辑或推迟到关联服务器的分析来做出此决定。

IdentityLookup 仅适用于来自未知发件人的短信和彩信,它不适用于联系人列表中发件人的消息、不适用任何 iMessage 消息、不适用于回复发件人 3 次及以上的会话。

image-20230526211245182.png

“消息” APP 使用一个 ILMessageFilterQueryRequest 对象将信息传递给 Message Filter Extension。Message Filter Extension 确定该消息的类别后,将 ILMessageFilterQueryResponse 对象返回给“消息” APP。

如果 App extension 无法自行做出决策,“消息” APP将会把有关信息发送到与 Message Filter Extension 关联的服务器,并将响应传递给 Message Filter Extension。Message Filter Extension 解析服务器的响应并返回最终的 ILMessageFilterQueryResponse 对象,如下图所示。

image-20230526211215132.png

出于隐私原因,系统会处理与关联的服务器的所有通信;Message Filter Extension 无法直接访问网络,也无法将数据写入应用的共享容器中。

消息过滤实践

为 APP 新增 Message Filter Extension:

image-20230526211910199.pngimage-20230526211953520.png

我们依次来看 MessageFilterExtension.swift 文件中的代码:

import IdentityLookup
final class MessageFilterExtension: ILMessageFilterExtension {}

ILMessageFilterExtension 是的主要类的抽象基类。在 Info.plist 中被设置 NSExtensionPrincipalClass,将在收到消息时被构造:

image-20230526212851516.png

ILMessageFilterExtension 类无其他要求或限制:

open class ILMessageFilterExtension : NSObject {
}

MessageFilterExtension 实现了 ILMessageFilterQueryHandlingILMessageFilterCapabilitiesQueryHandling 协议:

extension MessageFilterExtension: ILMessageFilterQueryHandling, ILMessageFilterCapabilitiesQueryHandling {
    // ...
}

ILMessageFilterQueryHandling

ILMessageFilterExtension 子类必须符合 ILMessageFilterQueryHandling协议,通过包含短信信息的 queryRequest 、提供请求关联网络服务器能力的 context,来进行短信类别的判断。最终返回提供包含类别信息的 response

@available(iOS 11.0, *)
public protocol ILMessageFilterQueryHandling : NSObjectProtocol {
    // 闭包
    func handle(_ queryRequest: ILMessageFilterQueryRequest, 
                context: ILMessageFilterExtensionContext, 
                completion: @escaping (ILMessageFilterQueryResponse) -> Void)
    // 异步函数
    func handle(_ queryRequest: ILMessageFilterQueryRequest, 
                context: ILMessageFilterExtensionContext
    ) async -> ILMessageFilterQueryResponse
}

queryRequest 的信息如下,包括发件人号码 sender、短信内容 messageBodyISO 国家代码 receiverISOCountryCode

@available(iOS 11.0, *)
open class ILMessageFilterQueryRequest : NSObject, NSSecureCoding {
    open var sender: String? { get }
    open var messageBody: String? { get }
    @available(iOS 16.0, *)
    open var receiverISOCountryCode: String? { get }
}

context 提供请求关联网络服务器能力,我们也只能使用该能力访问网络:

@available(iOS 11.0, *)
open class ILMessageFilterExtensionContext : NSExtensionContext {
    // 闭包
    open func deferQueryRequestToNetwork(completion: @escaping (ILNetworkResponse?, Error?) -> Void)
    // 异步函数
    open func deferQueryRequestToNetwork() async throws -> ILNetworkResponse
}

URL 记录在 Info.plistILMessageFilterExtensionNetworkURL 中,无法进行自定义。

image-20230526214659085.png

response 定义如下,需要提供对应的类别和子类别:

@available(iOS 11.0, *)
open class ILMessageFilterQueryResponse : NSObject, NSSecureCoding {
    open var action: ILMessageFilterAction
    @available(iOS 16.0, *)
    open var subAction: ILMessageFilterSubAction
}

noneallowjunkpromotiontransaction 类别,noneallow 的行为相同:

@available(iOS 11.0, *)
public enum ILMessageFilterAction : Int, @unchecked Sendable {
    case none = 0
    case allow = 1
    case junk = 2
    @available(iOS 14.0, *)
    case promotion = 3
    @available(iOS 14.0, *)
    case transaction = 4
}

以及文章开头提到的 12 种子类别:

@available(iOS 16.0, *)
public enum ILMessageFilterSubAction : Int, @unchecked Sendable {
    case none = 0
    
    /// TRANSACTIONAL SUB-ACTIONS
    
    case transactionalOthers = 10000
    case transactionalFinance = 10001
    case transactionalOrders = 10002
    case transactionalReminders = 10003
    case transactionalHealth = 10004
    case transactionalWeather = 10005
    case transactionalCarrier = 10006
    case transactionalRewards = 10007
    case transactionalPublicServices = 10008
    
    /// PROMOTIONAL SUB-ACTIONS
    
    case promotionalOffers = 20001
    case promotionalCoupons = 20002
}

因此,整体的过滤代码框架如下,依次进行设备端的检测、服务器检测:

func handle(_ queryRequest: ILMessageFilterQueryRequest,
            context: ILMessageFilterExtensionContext,
            completion: @escaping (ILMessageFilterQueryResponse) -> Void
) {
    // 设备端的检测
    let (offlineAction, offlineSubAction) = self.offlineAction(for: queryRequest)
    switch offlineAction {
    case .allow, .junk, .promotion, .transaction:
        let response = ILMessageFilterQueryResponse()
        response.action = offlineAction
        response.subAction = offlineSubAction
        completion(response)
    case .none:
       // 服务器检测
        context.deferQueryRequestToNetwork() { (networkResponse, error) in
            let response = ILMessageFilterQueryResponse()
            if let networkResponse = networkResponse {
                (response.action, response.subAction) = self.networkAction(for: networkResponse)
            }
            completion(response)
        }
    @unknown default:
        break
    }
}

这里需要注意,Apple 定义了服务器检测网络请求的格式,开发者无法进行自定义:

POST /server-endpoint HTTP/1.1
Accept: */*
Content-Type: application/json; charset=utf-8
Content-Length: 148
{
    "_version": 1,
    "query": {
        "sender": "14085550001",
        "message": {
            "text": "This is a message"
        }
    },
    "app": {
        "version": "1.1"
    }
}

ILMessageFilterCapabilitiesQueryHandling

ILMessageFilterCapabilitiesQueryHandling 协议会更简单些:

@available(iOS 16.0, *)
public protocol ILMessageFilterCapabilitiesQueryHandling : NSObjectProtocol {
    // 闭包
    func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest, 
                context: ILMessageFilterExtensionContext, 
                completion: @escaping (ILMessageFilterCapabilitiesQueryResponse
    ) -> Void)
    // 异步函数
    func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest, 
                context: ILMessageFilterExtensionContext
    ) async -> ILMessageFilterCapabilitiesQueryResponse
}

其中,capabilitiesQueryRequest 无实际含义,context 同前文。需要提供的是 ILMessageFilterCapabilitiesQueryResponse:

@available(iOS 16.0, *)
open class ILMessageFilterCapabilitiesQueryResponse : NSObject, NSSecureCoding {
}
​
@available(iOS 16.0, *)
@available(macOS, unavailable)
extension ILMessageFilterCapabilitiesQueryResponse {
​
    @nonobjc final public var transactionalSubActions: [ILMessageFilterSubAction]
​
    final public var promotionalSubActions: [ILMessageFilterSubAction]
}

指定了 Message Filter Extension 可以显示的子类别。我们可以这样展示以显示子类别:

func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest,
            context: ILMessageFilterExtensionContext,
            completion: @escaping (ILMessageFilterCapabilitiesQueryResponse) -> Void
) {
    let response = ILMessageFilterCapabilitiesQueryResponse()
    response.transactionalSubActions = [        .transactionalOthers,        .transactionalFinance,        .transactionalOrders,        .transactionalReminders,        .transactionalHealth,        .transactionalWeather,        .transactionalCarrier,        .transactionalRewards,        .transactionalPublicServices    ]
    response.promotionalSubActions = [                .promotionalOthers,        .promotionalOffers,        .promotionalCoupons,    ]
    completion(response)
}

在 iOS16 设备上,不同配置样式如下:

IMG_0381.PNGIMG_0379.PNGIMG_0380.PNG
未配置短信过滤配置短信过滤,无子类别配置短信过滤,展示所有子类别

垃圾短信和垃圾电话上报

此外,我们可以一个 App Extension,让用户将不需要的短信和电话上报为垃圾内容。上报电话需要用户在最近列表中进行左滑后选择报告。对于在消息记录中的短信,用户可以按下报告垃圾信息按钮:

IMG_9378.PNGIMG_9379.PNGIMG_9380.PNG

创建 Unwanted Communication Reporting Extension:

image-20230527145127105.pngimage-20230527145153818.png

我们可以看到模版代码十分简单:

class UnwantedCommunicationReportingExtension: ILClassificationUIExtensionViewController {
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Notify the system when you have completed gathering information
        // from the user and you are ready with a classification response
        self.extensionContext.isReadyForClassificationResponse = true
    }
    
    // Customize UI based on the classification request before the view is loaded
    override func prepare(for classificationRequest: ILClassificationRequest) {
        // Configure your views for the classification request
    }
    
    // Provide a classification response for the classification request
    override func classificationResponse(for request:ILClassificationRequest) -> ILClassificationResponse {
        return ILClassificationResponse(action: .reportJunk)
    }
}

当用户上报时,系统会启动 App Extension。搜集用户外信息后,进行后续的上报或阻止,如下图所示。

image-20230527150610691.png

具体来说,系统会依次:

  1. 实例化 App Extension 中的 ILClassificationUIExtensionViewController 子类。
  2. 调用实例的 prepare(for:)`方法并将控制器呈现给用户。
  1. 使用实例从用户那里收集数据,搜集完成 isReadyForClassificationResponse 设置为 true
  2. 如果用户按下取消按钮,系统将关闭 ILClassificationUIExtensionViewController 子类实例。

image-20230527152303432.png

  1. 如果用户按下完成,系统将调用 classificationResponse(for:) 方法,传入一个 ILClassificationRequest 对象。

image-20230527152436351.png

  1. 系统根据方法的 ILClassificationResponse 响应采取不同的操作。
@available(iOS 12.0, *)
open class ILClassificationResponse : NSObject, NSSecureCoding {
    open var action: ILClassificationAction { get }
    @available(iOS 12.1, *)
    open var userString: String?
    open var userInfo: [String : Any]?
    public init(action: ILClassificationAction)
}

ILClassificationAction 类型为:

/// Describes various classification actions.
@available(iOS 12.0, *)
public enum ILClassificationAction : Int, @unchecked Sendable {
    /// Indicate that no action is requested.
    case none = 0
    /// Report communication(s) as not junk.
    case reportNotJunk = 1
    /// Report communication(s) as junk.
    case reportJunk = 2
    /// Report communication(s) as junk and block the sender.
    case reportJunkAndBlockSender = 3
}

对于 ILClassificationAction.none,系统会关闭视图控制器,但不会采取任何其他操作。

对于 ILClassificationAction.reportNotJunkILClassificationAction.reportJunk,系统会根据 userInfo 属性生成报告,然后将其发布到扩展程序的 Info.plist 文件中指定的服务端(ILClassificationExtensionNetworkReportDestination)或者使用短信发到对应的号码(ILClassificationExtensionSMSReportDestination)。

image-20230527153304938.png

对于 ILClassificationAction.reportJunkAndBlockSender,系统的响应就像在 ILClassificationAction.reportJunk 操作中一样。 但是,在报告步骤之后,系统会发出提示,让用户知道该号码将被阻止(拉黑)。

最后,为了保护用户隐私,系统会在 App Extension 终止后删除该容器。有关详细信息,请参阅关于 iOS 文件系统

参考资料

基于短信过滤能力。上线了喵喵消烦员 App:

喵喵消烦员是一款短信过滤工具软件。在如今信息爆炸的时代,您的隐私和安全由喵喵来守护!我们使用 scikit-learn,通过朴素贝叶斯算法对垃圾短信进行识别,通过 Core ML 将模型部署在本地,从而完成离线过滤任务。

  1. 隐私安全:我们不会索要任何位置、相机、通知、无线数据(网络)等权限,用户的数据不会被保存,同时也不可能被上传,所有操作均在本地完成。
  2. 高效精准:通过先进的算法技术,自动识别并过滤掉垃圾短信、诈骗信息、广告推销等不必要打扰的内容。
  3. 自定义设置:支持自定义关键词过滤,方便您针对个人需求进行个性化设置,避免不必要的干扰。
  4. 更新迭代:我们会通过版本升级定期更新过滤模型,确保始终能够准确地识别和拦截最新的垃圾短信和诈骗信息。

模型、App 还在不断调整和优化,欢迎使用和提供建议!

image-20230527154027510.png