Facebook Messenger Bot Proactive/Push 使用 Azure 的通知

Facebook Messenger Bot Proactive/Push Notifications using Azure

我正在使用 Microsoft Bot Framework 为 Facebook Messenger 构建一个机器人。我计划使用 CosmosDB 进行状态管理,并作为我的后端数据存储。 (我不局限于 CosmosBD,如果需要可以使用任何其他商店)

我需要根据用户的时间偏好向他们发送 daily/weekly 主动消息(推送通知)。当他们第一次与机器人互动时,我会捕捉他们的时间偏好。

发送这些通知的最佳方式是什么?

因为我会将这些偏好存储在 CosmosDB 中,所以我正在考虑使用 ComosDB trigger of creating an Azure Function 并根据用户时间偏好安排它。此 Azure 函数将调用我的 webhook 来传递这些消息。如果需要,我会在用户更改 his/her 偏好时更改功能计划。

我的问题是:

  1. 这是一个好方法吗?
  2. 还有其他选择吗(通知中心?)
  3. 我应该能够设置特定的通知时间(比如在整点或类似的时间),在这些时间将 Azure 函数安排到 运行 比创建通知有意义吗基于用户偏好的功能(实际上我也可以将这两种方法结合起来)

提前致谢。

您可能想查看有关主动消息的 FB 政策 有 24 小时的限制,但在您的情况下可能不会完全搞砸

https://developers.facebook.com/docs/messenger-platform/policy/policy-overview#standard_messaging

我在 botframwork 和 Messenger 中广泛使用主动对话,没有任何问题。在您的 Facebook 批准过程中,您只需通知他们您将通过您的机器人的 Messenger 发送通知。通常,如果您使用它来通知您的用户并远离促销内容,您应该没问题。

我还使用 azure 函数从自定义控制器端点触发主动对话。

下面是 azure 函数的示例代码:

public static void Run(TimerInfo notificationTrigger, TraceWriter log)
{
    try
    {
        //Serialize request object
        string timerInfo = JsonConvert.SerializeObject(notificationTrigger);

        //Create a request for bot service with security token
        HttpRequestMessage hrm = new HttpRequestMessage()
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri(NotificationEndPointUrl),
            Content = new StringContent(timerInfo, Encoding.UTF8, "application/json")
        };
        hrm.Headers.Add("Authorization", NotificationApiKey);


        log.Info(JsonConvert.SerializeObject(hrm));

        //Call service
        using (var client = new HttpClient())
        {
            Task task = client.SendAsync(hrm).ContinueWith((taskResponse) =>
            {
                HttpResponseMessage result = taskResponse.Result;
                var jsonString = result.Content.ReadAsStringAsync();
                jsonString.Wait();

                if (result.StatusCode != System.Net.HttpStatusCode.OK)        
                {
                    //Throw what ever problem as an exception with  details
                    throw new Exception($"AzureFunction - ERRROR - HTTP {result.StatusCode}");
                }
            });

            task.Wait();
        }

    }
    catch (Exception ex)
    {
        //TODO log
    }
}

下面是启动主动对话的示例代码:

public static async Task Resume<T, R>(string resumptionCookie) where T : IDialog<R>, new()
{
    //Deserialize reference to conversation
    ConversationReference conversationReference = JsonConvert.DeserializeObject<ConversationReference>(resumptionCookie);

    //Generate message from bot to user
    var message = conversationReference.GetPostToBotMessage();

    var builder = new ContainerBuilder();
    using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
    {
        //From a cold start the service is not yet authenticated with dev bot azure services 
        //We thus must trust endpoint url.
        if (!MicrosoftAppCredentials.IsTrustedServiceUrl(message.ServiceUrl))
        {
            MicrosoftAppCredentials.TrustServiceUrl(message.ServiceUrl, DateTime.MaxValue);
        }


        var botData = scope.Resolve<IBotData>();
        await botData.LoadAsync(CancellationToken.None);

        //This is our dialog stack
        var task = scope.Resolve<IDialogTask>();

        T dialog = scope.Resolve<T>(); //Resolve the dialog using autofac        

        try
        {
            task.Call(dialog.Void<R, IMessageActivity>(), null);
            await task.PollAsync(CancellationToken.None);
        }
        catch (Exception ex)
        {
            //TODO log
        }
        finally
        {
            //flush dialog stack
            await botData.FlushAsync(CancellationToken.None);
        }
    }
}

您的对话框需要在 autofac 中注册。 您的 resumptionCookie 需要保存在您的数据库中。

首先,我认为这里没有 "right" 答案;这将在很大程度上取决于您域的特定需求。规模将在设计中发挥重要作用。你会有 100 个用户吗? 10000 个用户? 100 万用户?我假设您想预先设计最大规模,但这可能有点矫枉过正。

首先,根据您的描述,我认为 CosmosDB 触发器不一定是您问题的解决方案,因为它只会在偏好数据为 created/updated 时触发。我假设,从那时起,您的函数需要在他们选择的时间段内持续触发,对吗?

让我们假设您让人们选择一天中的 24 小时。一种天真的方法是简单地使用一个每小时触发一次的预定触发器,查询 CosmosDB 以查找首选项设置为该特定时间的所有文档,然后开始从那里发送通知。问题是你如何从那里扩展并处理失败时的幂等性问题。

首先,计时器触发器只会启动一个实例。如果您只是去查询 CosmosDB 文档并开始在该单个触发器的范围内一个接一个地处理它们,那么您将很快达到可以扩展到多少通知的上限。相反,您想要做的是使用该计时器触发器将通知分散到尽可能多的 "worker" 函数实例。计时器触发器可以充当编排器,因为它可以拥有针对 CosmosDB 的查询,然后将它在特定通知时间 window 找到的每个文档结果转换为它放置在队列中以供处理的消息通过一个单独的函数,它将自行扩展。

实际上有几种方法可以使用 Azure Functions 实现这一点,这实际上取决于您愿意多早采用技术。

第一种是我称之为 "manual" 的方法,只需使用现有的 Azure 存储队列扩展,将 IAsyncCollector<YourNotificationWorkerMessage> 作为绑定到的计时器函数的参数即可工人排队,然后通过它抽出消息。然后编写第二个使用 QueueTrigger 的伴随函数,将其绑定到同一个队列,它将负责处理每条消息。第二个函数是您进行缩放的地方,可以根据您选择配置的任何缩放参数尽快处理所有排队的消息。这是 "simplest" 方法

第二种方法是采用更新的 Durable Functions 扩展。使用该模型,您不必直接考虑创建工作队列。您只需从定时器函数启动一个新的协调器函数实例,然后协调器通过调用 N "concurrent" 调用每个通知的操作来展开工作。现在,它恰好在幕后使用队列来分发这些调用,但这是您不再需要自己维护的实现细节。此外,如果传递通知的工作需要更多涉及 and/or 重试逻辑的工作,您实际上可能会考虑使用子编排而不是简单的操作。最后,这种方法的另一个额外好处是,一旦所有通知都已传送,您就可以 "fan back in" 到您的主要协调器函数来做一些后续工作……即使这只是通知通知的某种事件日志记录这一小时的循环已完成。

现在,这两种方法中的任何一种所面临的挑战实际上是处理最初从 CosmosDB 中获取通知候选对象、分页结果并确保您实际上以幂等方式将它们全部展开的失败。你需要处理页面时可能出现的问题,你需要处理这样一个事实,即你的整个功能可能会被拆除,你可能不得不重新启动。也许在早上 8 点的初始 运行 通知中,你通过了 371 页中的第 273 页,然后你遇到了完全网络连接失败或者你的功能 运行 正在运行的 VM 遇到电源故障.您可以继续,但您需要知道您在第 273 页停止,并且您实际上处理了该页的第 27 条记录并从那里开始。否则,您可能会向用户发送双重通知。也许这是你可以接受的事情,也许不是。也许您可以接受该页面上的 27 个通知被复制,只要前 272 页不是。同样,这是你需要为你的域决定的事情,但如果你想避免这个问题,你的协调器功能将需要跟踪它的进度以确保它不会发送欺骗。我要再次强调,Durable Functions 在这方面有优势,因为它具有配置重试的能力。维护特定 运行 的状态由两种方法中的作者决定。