# Declared presence — schema for a campaign's Telegram surface. # # The instance lives in the campaign repo (pr-hall-of-helix/presence/telegram.yaml). # This file is the contract it is validated against, and lives here because the # provisioner is here. See docs/provisioning.md. $schema: "https://json-schema.org/draft/2020-12/schema" $id: "fluid-telegram/presence/telegram.schema.yaml" title: "Telegram declared presence" type: object required: [presence] additionalProperties: false properties: presence: type: object required: [schema_version, campaign, interface, bot, channels] additionalProperties: false properties: schema_version: { const: "0.1" } campaign: type: string description: > Campaign slug. Names the resolved-state file and the OpenBao subtree, so it must be stable for the life of the presence. pattern: "^[a-z0-9][a-z0-9-]*$" interface: type: string description: The FLUID interface that will publish through this presence. const: "helix-forge-telegram-publishing" bot: type: object required: [name, about, description] additionalProperties: false properties: name: type: string maxLength: 64 description: Display name. What a reader encountering it cold sees first. username_preference: type: array description: > Candidate usernames in order. BotFather requires a globally unique name ending in "bot" or "_bot", and the first choice is often taken. Declaring fallbacks keeps apply from stopping on a collision; the one actually issued is recorded in the resolved state. items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{3,30}([Bb]ot|_bot)$" } minItems: 1 about: type: string maxLength: 120 description: BotFather /setabouttext — shown on the profile before a chat starts. description: type: string maxLength: 512 description: BotFather /setdescription — shown in the empty chat. avatar: type: string description: > Repo-relative path to a square image in the campaign repo. Provisioning is content-addressed on its digest, so replacing the file is what triggers an update. channels: type: object required: [test, public] additionalProperties: false description: > Exactly two. The test channel is created first and is where every verification runs; nothing reaches the public channel until a person has looked at a rendering in the test channel (Canon PUB-01). properties: test: { $ref: "#/$defs/channel" } public: { $ref: "#/$defs/channel" } linked_discussion_group: type: boolean default: false description: > Whether the public channel has a linked group for comments. Off by default: comments are an inbound surface, and this interface is outbound only. Turning it on is a scope change, not a setting. $defs: channel: type: object required: [title, visibility] additionalProperties: false properties: title: type: string maxLength: 128 description: type: string maxLength: 255 visibility: enum: [private, public] username_preference: type: array description: Public channels only. Ordered candidates; a private channel must not declare one. items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{4,31}$" } admin_rights: type: array description: > Rights granted to the bot. The provisioner clamps this: post_messages is the only permitted value, and any other entry fails validation rather than being silently dropped. Declared explicitly so that a reader of the spec can see the boundary rather than infer it. items: { const: post_messages } default: [post_messages] maxItems: 1