”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 如何将通讯录与手机同步?在 Go 中实现 CardDAV!

如何将通讯录与手机同步?在 Go 中实现 CardDAV!

发布于2024-11-07
浏览:330

How to synchronize your contacts with your phone? Implemeting CardDAV in Go!

假设您帮助管理一个小型组织或俱乐部,并拥有一个存储所有会员详细信息(姓名、电话、电子邮件...)的数据库。
在您需要的任何地方都可以访问这些最新信息不是很好吗?好吧,有了 CardDAV,你就可以!

CardDAV 是一个得到良好支持的联系人管理开放标准;它在 iOS 联系人应用程序和许多适用于 Android 的应用程序中具有本机集成。

服务器端,实现 CardDAV 是一个 http 服务器,它响应不寻常的 http 方法(PROPFIND、REPORT 而不是 GET、POST...)。幸运的是,有一个 Go 模块可以大大简化工作:github.com/emersion/go-webdav。该库需要一个已实现的后端,并提供一个标准的 http.Handler,该处理程序应在身份验证后为 HTTP 请求提供服务。

验证

有趣的是,该库不提供有关用户身份验证的任何帮助,但是由于 Go 可组合性,这不是问题。
CardDAV 使用基本身份验证凭据。检查凭据后,我们可以将这些凭据保存在上下文中(稍后将有用):

package main

import (
    "context"
    "net/http"

    "github.com/emersion/go-webdav/carddav"
)

type (
    ctxKey   struct{}
    ctxValue struct {
        username string
    }
)

func NewCardDAVHandler() http.Handler {
    actualHandler := carddav.Handler{
        Backend: &ownBackend{},
    }

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        username, password, ok := r.BasicAuth()
        // check username and password: adjust the logic to your system (do NOT store passwords in plaintext)
        if !ok || username != "admin" || password != "s3cr3t" {
            // abort the request handling on failure
            w.Header().Add("WWW-Authenticate", `Basic realm="Please authenticate", charset="UTF-8"`)
            http.Error(w, "HTTP Basic auth is required", http.StatusUnauthorized)
            return
        }

        // user is authenticated: store this info in the context
        ctx := context.WithValue(r.Context(), ctxKey{}, ctxValue{username})
        // delegate the work to the CardDAV handle
        actualHandler.ServeHTTP(w, r.WithContext(ctx))
    })
}

实现 CardDAV 接口

ownBackend结构必须实现carddav.Backend接口,该接口不是很薄,但仍然可以管理。

CurrentUserPrincipal 和 AddressBookHomeSetPath 必须提供 URL(以斜线开头和结尾)。通常是用户名/联系人。这是您需要从上下文中提取用户名的地方(这是唯一可用的参数):

func currentUsername(ctx context.Context) (string, error) {
    if v, ok := ctx.Value(ctxKey{}).(ctxValue); ok {
        return v.username, nil
    }
    return "", errors.New("not authenticated")
}

type ownBackend struct{}

// must begin and end with a slash
func (b *ownBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
    username, err := currentUsername(ctx)
    return "/"   url.PathEscape(username)   "/", err
}

// must begin and end with a slash as well
func (b *ownBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
    principal, err := b.CurrentUserPrincipal(ctx)
    return principal   "contacts/", err
}

之后有趣的事情就开始了:您需要实现 AddressBook、GetAddressObject 和 ListAddressObjects 方法。

AddressBook 返回一个简单的结构,其中路径应以上面的 AddressBookHomeSetPath 开头(并以斜杠结尾)

GetAddressObject 和 ListAddressObjects 必须检查当前路径(以确保当前经过身份验证的用户可以访问这些联系人),然后将联系人作为 AddressObject 返回。

地址对象

AddressObject有多个属性,最重要的是:

  • 识别此特定联系人的路径(可以是任意的,以斜线开头)
  • ETag允许客户端快速检查是否发生任何更新(如果忘记,iOS将不会显示任何内容)
  • 需要 VCard 的卡

VCard 代表实际的联系人数据,并且可能必须根据您存储联系人的方式进行调整。就我而言,它是这样结束的:

func utf8Field(v string) *vcard.Field {
    return &vcard.Field{
        Value: v,
        Params: vcard.Params{
            "CHARSET": []string{"UTF-8"},
        },
    }
}

func vcardFromUser(u graphqlient.User) vcard.Card {
    c := vcard.Card{}

    c.Set(vcard.FieldFormattedName, utf8Field(u.Firstname " " u.Lastname))
    c.SetName(&vcard.Name{
        Field:      utf8Field(""),
        FamilyName: u.Lastname,
        GivenName:  u.Firstname,
    })
    c.SetRevision(u.UpdatedAt)
    c.SetValue(vcard.FieldUID, u.Extid)

    c.Set(vcard.FieldOrganization, utf8Field(u.Unit))

    // addFields sorts the key to ensure a stable order
    addFields := func(fieldName string, values map[string]string) {
        for _, k := range slices.Sorted(maps.Keys(values)) {
            v := values[k]
            c.Add(fieldName, &vcard.Field{
                Value: v,
                Params: vcard.Params{
                    vcard.ParamType: []string{k   ";CHARSET=UTF-8"}, // hacky but prevent maps ordering issues
                    // "CHARSET":       []string{"UTF-8"},
                },
            })
        }
    }

    addFields(vcard.FieldEmail, u.Emails)
    addFields(vcard.FieldTelephone, u.Phones)

    vcard.ToV4(c)
    return c
}

采用只读快捷方式

某些方法允许更新联系人。由于我不希望通过 CardDAV 更新我的成员列表,因此我向 Put 和 Delete 方法返回 403 错误: return webdav.NewHTTPError(http.StatusForbidden,errors.New("carddav:不支持操作"))

本地测试

iOS 要求 CardDAV 服务器通过 https 提供服务。您可以使用 openssl 在本地生成自签名证书(将 192.168.XXX.XXX 替换为您的 IP 地址),并将其输入到 http.ListenAndServeTLS(addr, "localhost.crt", "localhost.key", NewCardDAVHandler())

openssl req -new -subj "/C=US/ST=Utah/CN=192.168.XXX.XXX" -newkey rsa:2048 -nodes -keyout localhost.key -out localhost.csr
openssl x509 -req -days 365 -in localhost.csr -signkey localhost.key -out localhost.crt

之后,您应该能够通过添加指向您自己的 IP 地址和端口的“CardDAV 联系帐户”来在本地进行实验。

结论

在 Go 中实现 CardDAV 服务器有点复杂,但显然值得:您的联系人将自动与您组织服务器上的数据同步!

您知道其他允许这种本机集成的很酷的协议吗?欢迎分享您的经验!

版本声明 本文转载于:https://dev.to/cmdscale/how-to-synchronize-your-contacts-with-your-phone-implemeting-carddav-in-go-9ia?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何有效地转换PHP中的时区?
    如何有效地转换PHP中的时区?
    在PHP 利用dateTime对象和functions DateTime对象及其相应的功能别名为时区转换提供方便的方法。例如: //定义用户的时区 date_default_timezone_set('欧洲/伦敦'); //创建DateTime对象 $ dateTime = ne...
    编程 发布于2025-04-19
  • PHP中从MySQL函数迁移到PDO的有效方法
    PHP中从MySQL函数迁移到PDO的有效方法
    [2 从过时的mysql函数迁移以在PHP 旧的MySQL PHP扩展名被弃用并定于删除。 坚固且安全的替代方案是必不可少的,MySQLI和PDO_MYSQL都是可行的选择。 本指南重点介绍迁移到 pdo(php data对象)的优势。 建立PDO连接: 连接到MySQL数据库涉及实例化PDO对...
    编程 发布于2025-04-19
  • 在Python中如何创建动态变量?
    在Python中如何创建动态变量?
    在Python 中,动态创建变量的功能可以是一种强大的工具,尤其是在使用复杂的数据结构或算法时,Dynamic Variable Creation的动态变量创建。 Python提供了几种创造性的方法来实现这一目标。利用dictionaries 一种有效的方法是利用字典。字典允许您动态创建密钥并分...
    编程 发布于2025-04-19
  • 人脸检测失败原因及解决方案:Error -215
    人脸检测失败原因及解决方案:Error -215
    错误处理:解决“ error:( - 215)!empty()in Function openCv in Function MultSiscale中的“检测”中的错误:在功能检测中。”当Face Cascade分类器(即面部检测至关重要的组件)未正确加载时,通常会出现此错误。要解决此问题,必须...
    编程 发布于2025-04-19
  • JavaScript计算两个日期之间天数的方法
    JavaScript计算两个日期之间天数的方法
    How to Calculate the Difference Between Dates in JavascriptAs you attempt to determine the difference between two dates in Javascript, consider this s...
    编程 发布于2025-04-19
  • 左连接为何在右表WHERE子句过滤时像内连接?
    左连接为何在右表WHERE子句过滤时像内连接?
    左JOIN CONUNDRUM:WITCHING小时在数据库Wizard的领域中变成内在的加入很有趣,当将c.foobar条件放置在上面的Where子句中时,据说左联接似乎会转换为内部连接。仅当满足A.Foo和C.Foobar标准时,才会返回结果。为什么要变形?关键在于其中的子句。当左联接的右侧值...
    编程 发布于2025-04-19
  • 图片在Chrome中为何仍有边框?`border: none;`无效解决方案
    图片在Chrome中为何仍有边框?`border: none;`无效解决方案
    在chrome 中删除一个频繁的问题时,在与Chrome and IE9中的图像一起工作时,遇到了一个频繁的问题。和“边境:无;”在CSS中。要解决此问题,请考虑以下方法: Chrome具有忽略“ border:none; none;”的已知错误,风格。要解决此问题,请使用以下CSS ID块创建带...
    编程 发布于2025-04-19
  • MySQL中如何高效地根据两个条件INSERT或UPDATE行?
    MySQL中如何高效地根据两个条件INSERT或UPDATE行?
    在两个条件下插入或更新或更新 solution:的答案在于mysql的插入中...在重复键更新语法上。如果不存在匹配行或更新现有行,则此功能强大的功能可以通过插入新行来进行有效的数据操作。如果违反了唯一的密钥约束。实现所需的行为,该表必须具有唯一的键定义(在这种情况下为'名称'...
    编程 发布于2025-04-19
  • PHP与C++函数重载处理的区别
    PHP与C++函数重载处理的区别
    作为经验丰富的C开发人员脱离谜题,您可能会遇到功能超载的概念。这个概念虽然在C中普遍,但在PHP中构成了独特的挑战。让我们深入研究PHP功能过载的复杂性,并探索其提供的可能性。在PHP中理解php的方法在PHP中,函数超载的概念(如C等语言)不存在。函数签名仅由其名称定义,而与他们的参数列表无关。...
    编程 发布于2025-04-19
  • 如何使用PHP将斑点(图像)正确插入MySQL?
    如何使用PHP将斑点(图像)正确插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call ...
    编程 发布于2025-04-19
  • 如何从PHP中的数组中提取随机元素?
    如何从PHP中的数组中提取随机元素?
    从阵列中的随机选择,可以轻松从数组中获取随机项目。考虑以下数组:; 从此数组中检索一个随机项目,利用array_rand( array_rand()函数从数组返回一个随机键。通过将$项目数组索引使用此键,我们可以从数组中访问一个随机元素。这种方法为选择随机项目提供了一种直接且可靠的方法。
    编程 发布于2025-04-19
  • 您如何在Laravel Blade模板中定义变量?
    您如何在Laravel Blade模板中定义变量?
    在Laravel Blade模板中使用Elegance 在blade模板中如何分配变量对于存储以后使用的数据至关重要。在使用“ {{}}”分配变量的同时,它可能并不总是最优雅的解决方案。幸运的是,Blade通过@php Directive提供了更优雅的方法: $ old_section =“...
    编程 发布于2025-04-19
  • Python高效去除文本中HTML标签方法
    Python高效去除文本中HTML标签方法
    在Python中剥离HTML标签,以获取原始的文本表示 仅通过Python的MlStripper 来简化剥离过程,Python Standard库提供了一个专门的功能,MLSTREPERE,MLSTREPERIPLE,MLSTREPERE,MLSTREPERIPE,MLSTREPERCE,MLST...
    编程 发布于2025-04-19
  • 为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    为什么使用固定定位时,为什么具有100%网格板柱的网格超越身体?
    网格超过身体,用100%grid-template-columns 为什么在grid-template-colms中具有100%的显示器,当位置设置为设置的位置时,grid-template-colly修复了?问题: 考虑以下CSS和html: class =“ snippet-code”> g...
    编程 发布于2025-04-19
  • 如何在Java的全屏独家模式下处理用户输入?
    如何在Java的全屏独家模式下处理用户输入?
    Handling User Input in Full Screen Exclusive Mode in JavaIntroductionWhen running a Java application in full screen exclusive mode, the usual event ha...
    编程 发布于2025-04-19

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3