首页
/ Cal.com Telegram 集成解析:从声明式 App 配置到静态链接会议地点的完整实现

Cal.com Telegram 集成解析:从声明式 App 配置到静态链接会议地点的完整实现

2026-09-09 19:39:04作者:姚月梅Lane

Telegram 是 Cal.com 应用商店中一个典型的“消息/会议地点”型集成,它不依赖 OAuth、不需要服务端回调,而是以一条静态 Telegram 链接(https://t.me/...)作为预约事件的会议地点。本文以 packages/app-store/telegram/DESCRIPTION.md 为骨架,结合其 config.json、安装处理器 api/add.ts 以及应用商店地点注册逻辑 packages/app-store/locations.ts,讲透这一类“静态链接型”集成从声明、安装到校验、预订的全链路实现,读完即可照此模式理解或复刻同类型 App(如 WhatsApp、Campfire 等)。

一、这个集成解决什么问题

官方文档对该应用的定义只有一句话:

Schedule a chat with your guests or have a Telegram Video call.

即:在 Cal.com 的预约事件上,把 Telegram 用户名链接(https://t.me/MyUsername)作为会议地点。访客预约成功后,直接通过该链接发起 Telegram 聊天或视频通话,组织者无需把私人联系方式暴露在页面之外,也不存在“自动生成会议链接”的复杂度——链接由组织者在配置事件时手工填写。

从仓库结构看,该集成目录非常精简,只包含四个有效成员:

这种“配置驱动、零后端逻辑”的形态,正是 Cal.com App Store 中 linkType: "static" 一类应用的通用设计。

二、应用声明:config.json 逐字段拆解

config.json 是整个集成的元数据中枢,原文如下:

{
  "/*": "Don't modify slug - If required, do it using cli edit command",
  "name": "Telegram",
  "slug": "telegram",
  "type": "telegram_video",
  "logo": "icon.svg",
  "url": "https://cal.com/",
  "variant": "messaging",
  "categories": ["messaging"],
  "publisher": "Cal.com, Inc.",
  "email": "support@cal.com",
  "description": "Schedule a chat with your guests or have a Telegram Video call.",
  "__createdUsingCli": true,
  "appData": {
    "location": {
      "type": "integrations:telegram_video",
      "label": "Telegram",
      "linkType": "static",
      "organizerInputPlaceholder": "https://t.me/MyUsername",
      "urlRegExp": "^http(s)?:\\/\\/(www\\.)?t.me\\/[a-zA-Z0-9]*"
    }
  }
}

各字段含义与约束如下:

字段 说明
name Telegram 展示名,也是应用商店列表中的标题
slug telegram 全局唯一标识,文件头部注释明确警告:不要手工修改,需通过 CLI edit 命令变更
type telegram_video 凭据类型(Credential.type),写入数据库时使用
variant messaging 应用形态分类,安装处理器据此路由
categories ["messaging"] 应用商店分类
appData.location.type integrations:telegram_video 地点类型标识,是所有地点逻辑的匹配键
appData.location.linkType static 静态链接型:地点值由组织者手工填写
appData.location.organizerInputPlaceholder https://t.me/MyUsername 配置界面输入框的占位提示
appData.location.urlRegExp ^http(s)?:\/\/(www\.)?t.me\/[a-zA-Z0-9]* 链接格式校验正则

注意 typeappData.location.type 是两个不同的键:前者 telegram_video 用于凭据(Credential)记录,后者 integrations:telegram_video 用于地点(Location)匹配,二者通过安装处理器桥接。

三、安装即声明:零 OAuth 的声明式安装处理器

Telegram 不需要授权码、API Key 或回调地址,因此其安装逻辑完全由声明式处理器承担。api/add.ts 全文如下:

import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";

import { createDefaultInstallation } from "../../_utils/installation";
import appConfig from "../config.json";

const handler: AppDeclarativeHandler = {
  appType: appConfig.type,
  variant: appConfig.variant,
  slug: appConfig.slug,
  supportsMultipleInstalls: false,
  handlerType: "add",
  createCredential: ({ appType, user, slug, teamId }) =>
    createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};

export default handler;

关键点:

  • appTypevariantslug 全部直接取自 config.json,保证配置与逻辑不脱节;
  • supportsMultipleInstalls: false——同一用户/团队只允许安装一次,重复安装会被拒绝;
  • handlerType: "add" 表明这是“添加应用”处理器;
  • createCredential 调用 packages/app-store/_utils/installation.ts 中的 createDefaultInstallation,写入一条空 keykey: {})的 Credential 记录,这印证了“无凭据凭据”的设计——Telegram 应用本身不需要任何密钥。

createDefaultInstallation 底层执行:

const installation = await prisma.credential.create({
  data: {
    type: appType,      // "telegram_video"
    key,                // {} 空对象
    ...(teamId ? { teamId } : { userId: user.id }),
    appId: slug,        // "telegram"
    subscriptionId,
    paymentStatus,
    billingCycleStart,
  },
});

即:安装动作等价于在 Credential 表中插入一条 type = telegram_videoappId = telegram 的记录,可归属个人(userId)或团队(teamId)。而 api/index.ts 仅做了一件事:export { default as add } from "./add",把该处理器注册到应用商店 API 入口。

四、从 App 元数据到“事件地点”:locations.ts 的动态注册机制

安装之后,Telegram 如何成为事件地点?答案在 packages/app-store/locations.ts 的启动期扫描逻辑:

for (const [appName, meta] of Object.entries(appStoreMetadata)) {
  const location = meta.appData?.location;
  if (location) {
    // 模板变量替换:{SLUG} -> slug, {TITLE} -> name
    // 归一化默认值
    const newLocation = {
      ...location,
      messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`,
      iconUrl: meta.logo,
      variable: location.variable || "locationLink",
      defaultValueVariable: location.defaultValueVariable || "link",
    };
    // 静态链接型:强制要求组织者输入
    if (newLocation.linkType === "static") {
      newLocation.organizerInputType = location.organizerInputType || "text";
      ...
    }
    locationsFromApps.push(newLocation);
  }
}
const locations = [...defaultLocations, ...locationsFromApps];

对 Telegram 而言,扫描后的地点对象等价于:

{
  type: "integrations:telegram_video",
  label: "Telegram",
  linkType: "static",
  organizerInputPlaceholder: " https://t.me/MyUsername", // 前导空格是为了避免翻译层吞掉 https://
  urlRegExp: "^http(s)?:\\/\\/(www\\.)?t.me\\/[a-zA-Z0-9]*",
  organizerInputType: "text",
  variable: "locationLink",
  defaultValueVariable: "link",
  iconUrl: "icon.svg",
}

这里有个值得注意的实现细节:linkType: "static" 会强制 organizerInputType = "text",即配置事件时必须由组织者手动填写链接;而 getLocationValueForDB(同文件 locations.ts)在落库时会把地点值替换为 link 字段,存入预订的 location 列。

五、链接校验:urlRegExp 与 zod 双层把关

静态链接型应用在保存事件地点时,会通过 locations.ts 中的 locationsResolver 进行 zod 校验:

.superRefine((val, ctx) => {
  if (val?.link) {
    const eventLocationType = getLocationByType(val.type);
    if (eventLocationType && !eventLocationType.default &&
        eventLocationType.linkType === "static" && eventLocationType.urlRegExp) {
      const valid = z.string()
        .regex(new RegExp(eventLocationType.urlRegExp))
        .safeParse(link).success;
      if (!valid) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path: [eventLocationType?.defaultValueVariable ?? "link"],
          message: t("invalid_url_error_message", {
            label: eventLocationType.label,
            sampleUrl: sampleUrl ?? "https://cal.com",
          }),
        });
      }
    }
  }
});

对 Telegram,即校验填写值必须匹配:

^http(s)?:\/\/(www\.)?t.me\/[a-zA-Z0-9]*

这条正则允许 http/https、可选的 www. 前缀,并要求路径为 t.me/<字母数字用户名>。不满足时,界面会提示“无效链接”,并给出示例 https://t.me/MyUsername(即 organizerInputPlaceholder)。除 superRefine 外,guessEventLocationType / getStaticLinkLocationByValuelocations.ts)还支持按值反查地点类型:即使历史数据只存了链接字符串而非类型,也能通过 urlRegExp 匹配回 integrations:telegram_video

六、预订与展示链路:组织者视角的完整流程

综合以上源码,一次完整的 Telegram 会议预订流程为:

  1. 安装:用户在应用商店点击安装,add 处理器写入空 key 的 Credential(api/add.ts);
  2. 配置事件:编辑事件类型时选择“Telegram”地点,按占位提示填入 https://t.me/MyUsername,可选勾选“在预订页公开显示”;locationsResolverurlRegExp 校验合法性;
  3. 落库getLocationValueForDB 将地点类型解析为 link 值存入预订(locations.ts);
  4. 展示:预订页/确认页通过 getHumanReadableLocationValuegetSuccessPageLocationMessagelocations.ts)将存储值渲染为可点击的 Telegram 链接;
  5. 隐私:若组织者未开启“公开显示”,privacyFilteredLocationslocations.ts)会在对访客展示时剥掉 link 字段,仅在确认邮件中给出真实链接。

七、写在最后:如何复刻同类型集成

从 Telegram 这个最小范例可以提炼出“静态链接型”App 的完整实现模板:

  1. 建立目录 packages/app-store/<slug>/,包含 config.jsonindex.tsapi/add.tsDESCRIPTION.mdstatic/ 素材;
  2. config.jsonappData.location 中声明 linkType: "static"organizerInputPlaceholderurlRegExp
  3. api/add.ts 复用 createDefaultInstallation,无需任何后端鉴权逻辑;
  4. 地点注册、表单校验、落库与展示全部由 packages/app-store/locations.tspackages/app-store/_utils/installation.ts 的统一机制自动完成。

仓库中 linkType: "static" 的同类应用(如 integrations:whatsappintegrations:campfire_video 等,见 packages/app-store/locations.ts 的注册循环)遵循完全一致的套路。理解 Telegram 这一个例子,就等于掌握了 Cal.com 应用商店中最大的一个集成子类。

Cal.com 编辑事件地点:选择 Telegram 并填写 t.me 链接

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525