【问题标题】:Hooking to Zapier using .Net WebHooks as RESThooks使用 .Net WebHooks 作为 RESThooks 连接到 Zapier
【发布时间】:2016-05-14 08:59:35
【问题描述】:

我正在研究创建一个“Zap 应用程序”,并且想知道是否有人使用新的 .Net Webhooks 这样做了。它们似乎具有 RESTHooks 要求的“模式”,即订阅/发布机制。它工作的例子并不多,我想在我花了几天时间实施它并发现它不兼容之前检查一下。

挂钩到 Zapier 的实际代码示例会很棒!

【问题讨论】:

    标签: .net rest webhooks zapier


    【解决方案1】:

    进行了一些研究,但我终于让 Zapier Rest Hooks 工作了。不像我希望的那样直截了当(更有可能我的吸收速度有点慢)。客户支持非常出色且友好,因此请随时通过电子邮件向他们发送您的问题。此外,一旦你开始使用它,它就会非常强大,尽管它们的正常 webhook 机制也可以工作,并且不需要你创建 Zap 应用程序。在撰写本文时,我还没有推出该应用程序,尽管它在本地运行。这假设您已经开始在开发人员仪表板中创建自己的 Zapier 应用程序。这很简单,所以我不会在这里介绍。

    本说明仅涵盖创建单个触发器(尽管您创建了另外 1 个私有触发器以支持用户身份验证,而我创建了另一个触发器用于动态下拉)并且仅作为具有基本身份验证的 RESThook。基础是:

    1. 创建 webhook 以允许 Zapier 添加、更新和删除对您的操作的订阅。通过“订阅”,Zapier 不必轮询您的 webhook,而是当订阅操作发生在您身边时,您会响应 Zapier 在订阅过程中为您提供的 URL。

    2. 创建一个记录这些订阅的数据库表,存储您需要的数据然后发回响应当您触发操作时,发送到 Zapier 提供的 URL。

    3. 触发操作时,识别并发布您告诉 zapier 您将发送的数据。 Zapier 非常聪明,会为您映射数据(JSON 或 XML),因此当连接到另一个应用程序时,用户可以在两者之间进行映射。

    所以还有一些细节。这是在 .Net 上的 C# 中完成的,但我认为这些概念在任何其他语言或平台上都应该同样适用。

    首先是 RESTHook。以下是 RESTHook 方法的示例。请注意,我花了几天时间试图弄清楚 Zapier 脚本方面,所以我对命名约定并不完全满意,但希望你能明白。

    在这种情况下,有一个“表单”的概念,它是一个 JSON 数据块,其中包含我关心的数据、用户所属的用户和帐户。它们在系统中都有一个唯一的 ID。最后,订阅本身有一个 id。当您订阅时,特定帐户中的特定用户正在订阅特定表单,当特定触发器(该表单的提交)完成时将发送到 Zapier。

    RESTHooks:

    首先是RouteConfig,它将路径映射到要执行的方法。你可以看到我实现的所有方法。其中一些没有被使用,只是包括在未来可能的使用中(比如更新订阅)。

        // ZAPIER Webhooks
        routes.MapRoute(
           "User_Form_List",
           "api/zapier/user/formlist",
            new { controller = "Zapier", action = "RetrieveFormListForUser" },
            new { httpMethod = new HttpMethodConstraint("GET") }
            );
        routes.MapRoute(
           "Authenticate_Subscription",
           "api/zapier/authenticate",
            new { controller = "Zapier", action = "WebhookAuthenticate" },
            new { httpMethod = new HttpMethodConstraint("GET") }
            );
        routes.MapRoute(
            "Test_Subscription",
            "api/zapier/subscription/testdata",
            new { controller = "Zapier", action = "TestData" },
            new { httpMethod = new HttpMethodConstraint("GET") }
            );
        routes.MapRoute(
            "Create_Submission",
            "api/zapier/subscription/create",
            new { controller = "Zapier", action = "CreateSubscription" },
            new { httpMethod = new HttpMethodConstraint("GET") }
            );
        routes.MapRoute(
           "List_Subscriptions",
           "api/zapier/subscription",
           new { controller = "Zapier", action = "ListSubscriptions" },
           new { httpMethod = new HttpMethodConstraint("GET") }
           );
        routes.MapRoute(
           "Get_Subscriptions",
           "api/zapier/subscription/{id}",
           new { controller = "Zapier", action = "GetSubscription", id = 0 },
           new { httpMethod = new HttpMethodConstraint("GET") }
           );
        routes.MapRoute(
           "Update_Subscription",
           "api/zapier/subscription/{id}",
           new { controller = "Zapier", action = "UpdateSubscription", id = 0 },
           new { httpMethod = new HttpMethodConstraint("PUT") }
           );
        routes.MapRoute(
            "Delete_Subscription",
            "api/zapier/subscription/{id}",
            new { controller = "Zapier", action = "DeleteSubscription", id = 0 },
            new { httpMethod = new HttpMethodConstraint("DELETE") }
        );
    

    与此对应的代码是(另外我已经把错误处理拉出来以减少代码大小):

        public class ZapierController : BaseController //(this inherits from Controller)
        {
            private readonly IMyRepository _DBrepository;
    
            public ZapierController(IMyRepository repository, ...lots of other autowiring you don't need or care about)
                : base(logger)
            {
                _DBrepository = repository;
             }
    
            #region Zapier Subscriptions
    
            // api/zapier/subscription/create  : Creates a subscription
            [HttpGet]
            public ActionResult CreateSubscription()
            {
                ApiResult authresult = Authenticate();
                if (authresult.code != 201)
                {
                    return JsonApiResult(authresult);
                }
    
                // Get the request parameters
                var reqParams = GetParameters();
    
                // Create the subscription so long as it does not already exist
                WebhookSubscription sub = new WebhookSubscription();
                // _currentUser and _currentAccount are set as part of the authenticate and stored in our base controller
                sub.AccountId = _currentAccount.Id;
                sub.UserId = _currentUser.UserId;
                sub.TargetURL = reqParams["target_url"];
                sub.EventType = reqParams["target_event"];
                sub.FormId = Int32.Parse(reqParams["form_id"]);
                sub.IsActive = true;
    
                ObjectResult workflowActionRecord = _DBrepository.createWebhookSubscription(sub);
                sub.Id = workflowActionRecord.objectId;
    
                // return the subscription back to Zapier in the result. Zapier will remember it
                var result = new ApiResult();
                result.data.id = workflowActionRecord.objectId;
                result.data.subscription = sub;
                result.code = 201;
                return JsonApiResult(result);
            }
    
    
            // api/zapier/authenticate  : used to test authentication
            [HttpGet]
            public ActionResult WebhookAuthenticate()
            {
                ApiResult authresult = Authenticate();
    
                var result = new ApiResult();
                result.code = 201;
                return JsonApiResult(result);
            }
    
            // api/zapier/user/formlist  : returns list of forms for this user
            [HttpGet]
            public ActionResult RetrieveFormListForUser()
            {
                ApiResult authresult = Authenticate();
    
                var result = new ApiResult();
    
                List<Form> forms = _DBRepository.FormListRetrieveByUser(_currentUser, false);
    
                JsonSerializer serializer = new JsonSerializer();
                serializer.Converters.Add(new JavaScriptDateTimeConverter());
                serializer.NullValueHandling = NullValueHandling.Ignore;
    
                // Again Zapier likes arrays returned
                JArray objarray = JArray.FromObject(forms);
                return JsonApiResultDynamic(objarray);
            }
    
            // api/zapier/subscription/testdata  : returns test data for zapier
            [HttpGet]
            public ActionResult TestData()
            {
    
                ApiResult authresult = Authenticate();
    
                var result = new ApiResult();
    
                JsonSerializer serializer = new JsonSerializer();
                serializer.Converters.Add(new JavaScriptDateTimeConverter());
                serializer.NullValueHandling = NullValueHandling.Ignore;
    
                // Get the request parameters
                var reqParams = GetParameters();
                int chosenFormId = -1;
                // We need the form Id to proceed
                if (reqParams != null && reqParams["form_id"] != null)
                    chosenFormId = Int32.Parse(reqParams["form_id"]);
                else
                    return  JsonApiResult(new ApiResult() { code = 403, error = "Form Id Not Found" });
    
                // Get the form by Form Id, and return the JSON...I have removed that code, but make sure the result is place in an Array
                var resultdata = new[] { myFinalFormJSON };
    
                JArray objarray = JArray.FromObject(resultdata);
                return JsonApiResultDynamic(objarray);
            }
    
    
            // api/zapier/subscription  : returns list of subscriptions by account
            [HttpGet]
            public ActionResult ListSubscriptions()
            {
                ApiResult authresult = Authenticate();
    
                // Get a list all subscriptions for the account
                List<WebhookSubscription> actionData = _DBrepository.accountWebhookSubscriptions(_currentAccount.Id);
    
                var result = new ApiResult();
                result.code = 201;
                result.data.subscriptions = actionData;
                return JsonApiResult(result);
            }
    
            // api/zapier/subscription/{id}  : Creates a subscription
            [HttpGet]
            public ActionResult GetSubscription(int id)
            {
                ApiResult authresult = Authenticate();
    
                // Get a list all subscriptions for the account
                WebhookSubscription actionData = _DBrepository.getWebhookSubscription(id);
    
                var result = new ApiResult();
                result.data.subscription = actionData; ;
                result.code = 201;
                return JsonApiResult(result);
            }
    
            // api/zapier/subscription/{id}  : updates a subscription
            [HttpPut]
            public ActionResult UpdateSubscription(int id)
            {
                ApiResult authresult = Authenticate();
    
                // get target url and eventy type from the body of request
                string jsonString = RequestBody();
                var json = CommonUtils.DecodeJson(jsonString);
    
                // Create the subscription so long as it does not already exist
                WebhookSubscription sub = _DBrepository.getWebhookSubscription(id);
    
                var result = new ApiResult();
                if (sub != null)
                {
                    sub.TargetURL = json.target_url; ;
                    sub.EventType = json.eventType;
    
                    ObjectResult objResult = _DBrepository.updateWebhookSubscription(sub);
                    result.code = 201;
                }
    
                return JsonApiResult(result);
            }
    
    
    
            // api/zapier/subscription/{id}  : deletes a subscription
            [HttpDelete]
            public ActionResult DeleteSubscription(int id)
            {
                ApiResult authresult = Authenticate();
    
                // Delete a subscription
                _DBrepository.deleteWebhookSubscription(id);
    
                var result = new ApiResult();
                result.code = 201;
                return JsonApiResult(result);
            }
    
            // We need to Basic Auth for each call to subscription
            public ApiResult Authenticate()
            {
                // get auth from basic authentication header
                var auth = this.BasicAuthHeaderValue();
    
                // parse credentials from auth
                var userCredentials = Encoding.UTF8.GetString(Convert.FromBase64String(auth));
                var parts = CommonUtils.SplitOnFirst(userCredentials, ":");
                var username = parts[0];
                var password = parts[1];
    
                // authenticate user against repository
                if (!_DBrepository.UserAuthenticate(username, password))
                {
                    _logger.Info("Invalid Authentication: " + username);
                    return new ApiResult() { code = 401, error = "invalid authentication" };
                }
    
                return new ApiResult() { code = 201, error = "successful authentication" };
    
            }
       }
    

    将保存订阅的数据库表如下所示。我将省略阅读和写作方面,因为您可能有不同的机制。

       Create.Table("WebhookSubscription")
            .WithColumn("Id").AsInt32().Identity().PrimaryKey().NotNullable()
            .WithColumn("AccountId").AsInt32().NotNullable()
            .WithColumn("UserId").AsInt32().NotNullable()
            .WithColumn("EventType").AsString(256).NotNullable()
            .WithColumn("TargetURL").AsString(1000).NotNullable()
            .WithColumn("IsActive").AsBoolean().NotNullable()
            .WithColumn("CreatedOn").AsDateTime().Nullable()
            .WithColumn("FormId").AsInt32().NotNullable().WithDefaultValue(0);
            .WithColumn("UpdatedAt").AsDateTime().Nullable();
    

    为了清楚起见,以下列的含义/用途:

    • Id - 唯一订阅 ID。将用于退订
    • AccountId - 订阅用户的帐户 ID。如果您希望所有这些都在帐户级别上运行,则可以改为这样做
    • UserId - 订阅用户的 ID
    • EventType - 你的 Action 要响应的事件类型,例如“new_form_submission”
    • TargetURL - zapier 在订阅时为您提供的目标 URL。将在您启动操作时发布 JSON 的位置
    • FormId - 提交时用户想要操作的表单的 ID

    这就是订阅所需的代码(显然你不能把它扔到那里然后让它工作——为了节省空间而留下了很多)...

    触发代码

    只剩下代码是实际的触发器代码 - 当您的代码中遇到正在查找的事件时您执行的代码。因此,例如,当用户提交“表单”时,我们希望将该表单 JSON 发送到 Zapier。现在我们已经设置了所有其他代码,这部分非常简单。首先,检测我们收到需要 Zapier 响应的提交的代码:

    查看表单提交是否注册/订阅到 Zapier 的实际代码:

    public BusinessResult FormSubmitted(string jsonString)
    {
        var json = CommonUtils.DecodeJson(jsonString);
        var account = _DBrepository.AccountRetrieveById(_currentUser.AccountId.Value); // Assumes user has bee authenticated
    
    
        // inject additional meta data into json and retrieve submission/alert settings
        var form = _DBformRepository.FormRetrieveById((int)json.formId);
    
        // Lookup Subscription Webhooks
        List<WebhookSubscription>  subscriptions = _DBrepository.accountWebhookSubscriptions(account.Id);
        if (subscriptions != null && subscriptions.Count > 0)
        {
            foreach (WebhookSubscription sub in subscriptions)
            {
                if (sub.EventType.Equals("new_form_submission") && sub.FormId == form.Id)
                {
                    _webhookService.NewFormSubmission(sub, jsonString, form.Name, account.Name, account.Id);
                }
            }
        }
    }
    

    最后是将该响应发回给 Zapier 的代码,Zapier 将解析 JSON 并将其发送给相关方:

    public class WebhookService : IWebhookService
    {
        protected readonly IRepository _DBrepository;
    
        public WebhookService(IRepository repository)
        {
            _DBrepository = repository;
        }
    
        public void NewFormSubmission(string formResultJSON)
        {
            throw new NotImplementedException();
        }
    
        public void NewFormSubmission(WebhookSubscription subscription, string formResultJSON, string formName, string accountName, int accountId)
        {
            // Now post to webhook URL
            string response; 
            using (var client = new WebClient())
            {
                client.Headers[HttpRequestHeader.ContentType] = "application/json";
                // Needs to be an array sent to Zapier
                response = client.UploadString(subscription.TargetURL, "POST", "[" + formResultJSON + "]");
            }
        }
    }
    

    好的,这应该可以帮助您完成大部分工作。但是将代码/webhook 连接到 Zapier 是比较困难的地方。现在的想法是使用开发仪表板将上面的代码连接到您的 Zapier 应用程序中。您必须开始创建 Zapier 应用程序。您需要 2 个主要触发器 - 您尝试实施的基本操作(在本例中为“新表单提交”)和身份验证,以便 Zapier 在创建 Zap 时对用户进行身份验证(在本例中为“测试身份验证”) .我正在使用基本身份验证,但支持其他身份验证(OAuth 等)。此外,我添加了一个触发器,它将返回用户有权访问的表单列表。由于它不是必需的,因此我不会在屏幕截图中显示该实现:

    我不会展示“Test Auth”的接线,因为它进行得很顺利(如果有人要求,我会添加它 - 天知道是否有人会读到这个)。所以这里是“新表单提交”的逐页布线:

    第 1 页

    第 2 页

    这是我连接表单列表的地方,它提供了创建 Zap 的用户可以从中选择的表单列表。除非您有要显示的动态数据,否则您可能可以跳过它(将其留空)。为了完整起见,我将其包括在内:

    第 3 页

    你在这里连接测试数据

    第 4 页

    此页面允许您输入示例数据。跳过这个,因为它非常简单。

    脚本 API

    现在您已经连接了您的第一个 Zap 触发器!但是等等,我们还没有完成。为了使订阅过程正常工作,我们需要添加一些脚本。这是整个过程中最难的部分,而且不是很直观。所以在原来的主屏幕上,往下一点你会看到Scripting API

    现在您必须拥有 RESTHook 订阅的脚本。由于 Zapier 确实有这方面的文档,因此我不会详细介绍,但很高兴知道 Zapier 确实将数据存储为订阅的一部分。此外,我们还需要在此之后再进行一次接线步骤...

    var Zap = {
        pre_subscribe: function(bundle) {
            bundle.request.method = 'GET';
            bundle.request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            bundle.request.params = {
                target_url: bundle.subscription_url,
                target_event:bundle.event,
                form_id:bundle.trigger_fields.form_id
            };
            bundle.request.data = $.param({
                target_url: bundle.subscription_url,
                target_event:bundle.event,
                form_id:bundle.trigger_fields.form_id
            });
            return bundle.request;
        },
        post_subscribe: function(bundle) {
            // must return a json serializable object for use in pre_unsubscribe
            var data = JSON.parse(bundle.response.content);
            // we need this in order to build the {{webhook_id}}
            // in the rest hook unsubscribe url
            return {webhook_id: data.id};
        },
        pre_unsubscribe: function(bundle) {
            bundle.request.method = 'DELETE';
            bundle.request.data = null;
            return bundle.request;
        },
        new_form_submission_pre_poll: function(bundle) { 
            bundle.request.method = 'GET';
            bundle.request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            bundle.request.params = bundle.trigger_fields;
            bundle.request.data = $.param({
                form_id:bundle.trigger_fields.form_id
            });
            return bundle.request;
        }
    };
    

    这有点...但是查看 Zapier 文档应该会有所帮助。或者在这里发布问题,我会尽力回答......这比我预期的要大!

    管理触发器设置

    最后,我们需要完成订阅的接线...

    然后我们设置我们之前创建的 RESTHook 方法:

    就是这样。希望这可以节省一些时间和学习经验!

    【讨论】:

    • 非常感谢您的详细回答!!你能发布你的基本控制器吗?
    • 您能在本地完成这项工作吗?就像在 localhost 上测试你的 API 一样?如果是这样的话。由于 Zapier 似乎不支持这使得调试变得相当困难?
    • @user111232 我能够在本地运行他们的调试版本——但对 Zapier 的调用仍然是一个黑匣子。我尽可能使用 PostMan 来模仿他们的一面。令人失望的是,他们的东西非常神秘,但他们反应灵敏。我什至现在有一个问题,即在同一帐户上创建第二次登录并失败。然而,在生产中调试几乎是不可能的。至于 BasController……你到底在找什么?
    • 在我开始尝试使用这些之前,这条评论在哪里。超级好用
    • @Harm 只是我们在内部创建的一个类,用于保存对 API 的调用结果。认为它包含 http 响应代码和其他一些垃圾。您可以忽略或替换它。这段代码也有点陈旧——我认为有一些更新,但我不再参与那个项​​目。如果您遇到困难,请告诉我,我会尽力而为
    猜你喜欢
    • 2021-06-26
    • 2019-01-19
    • 2017-06-28
    • 2020-07-17
    • 2017-10-20
    • 2021-11-06
    • 2017-08-26
    • 2019-10-08
    • 2019-06-10
    相关资源
    最近更新 更多