prepare plugin for obsidian community registry

- rename plugin to telegram-bridge (drop 'obsidian' prefix per guidelines)
- promote manifest.json + versions.json to repo root
- replace settings h2/h3/h4 with Setting.setHeading()
- drop plugin-name prefix from command titles, add reconnect and sign-out
- wire SyncEngine polling through plugin.registerInterval
- replace console.log with Notice in show-client-id
- move inline usage-bar styles to CSS custom properties
- gate status bar item on Platform.isDesktopApp
- add first-run supabase requirement disclosure
- add tag-triggered GitHub release workflow
This commit is contained in:
tmlnv 2026-04-19 20:02:58 +03:00
parent f7330c8e29
commit d8509e7111
11 changed files with 182 additions and 33 deletions

59
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: plugin/package-lock.json
- name: Install plugin dependencies
run: npm ci
working-directory: plugin
- name: Build plugin
run: npm run build
working-directory: plugin
- name: Verify manifest version matches tag
run: |
TAG="${GITHUB_REF_NAME}"
MANIFEST_VERSION="$(node -p "require('./manifest.json').version")"
if [ "$TAG" != "$MANIFEST_VERSION" ]; then
echo "Tag $TAG does not match manifest.json version $MANIFEST_VERSION" >&2
exit 1
fi
- name: Assemble release assets
run: |
mkdir -p release
cp manifest.json release/manifest.json
cp plugin/main.js release/main.js
cp plugin/styles.css release/styles.css
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
files: |
release/manifest.json
release/main.js
release/styles.css

View file

@ -1,4 +1,4 @@
# Obsidian Telegram Sync
# Telegram Bridge for Obsidian
Sync messages from a Telegram bot into your Obsidian vault. Messages arrive in real time, are stored in Supabase, and are pulled into your vault as Markdown notes — with support for files, forum topics, message edits, and flexible routing rules.
@ -144,12 +144,12 @@ Until this plugin is published to the Obsidian community registry, install it ma
```
2. Copy the output files into your vault's plugin folder:
```
<vault>/.obsidian/plugins/obsidian-telegram/
├── main.js
├── manifest.json
└── styles.css (if present)
<vault>/.obsidian/plugins/telegram-bridge/
├── main.js (from plugin/main.js)
├── manifest.json (from repo root)
└── styles.css (from plugin/styles.css)
```
3. In Obsidian → **Settings → Community plugins**, enable **Obsidian Telegram Sync**.
3. In Obsidian → **Settings → Community plugins**, enable **Telegram Bridge**.
### 3.2 Connect to Supabase
@ -321,7 +321,11 @@ The Telegram Bot API limits file downloads to **20 MB**. Anything larger cannot
```
obsidian-telegram/
├── manifest.json Obsidian plugin manifest (release source of truth)
├── versions.json Plugin → min Obsidian app version map
├── plugin/ Obsidian plugin (TypeScript + esbuild)
│ ├── manifest.json Local-dev mirror of the root manifest
│ ├── styles.css Plugin styles
│ └── src/
│ ├── main.ts Plugin entry point
│ ├── sync-engine.ts Polling, cursor management, Realtime

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "telegram-bridge",
"name": "Telegram Bridge",
"version": "0.1.0",
"minAppVersion": "1.5.0",
"description": "Sync messages from a Telegram bot into your vault through a self-hosted Supabase backend.",
"author": "tmlnv",
"authorUrl": "https://github.com/tmlnv/obsidian-telegram",
"isDesktopOnly": false
}

View file

@ -1,8 +1,8 @@
{
"name": "obsidian-telegram",
"name": "telegram-bridge",
"version": "0.1.0",
"private": true,
"description": "Monorepo for the Obsidian Telegram plugin and Supabase backend",
"description": "Monorepo for the Telegram Bridge Obsidian plugin and Supabase backend",
"scripts": {
"verify": "node scripts/verify-local-env.mjs",
"bootstrap:supabase": "node scripts/bootstrap-supabase.mjs",

View file

@ -1,9 +1,9 @@
{
"id": "obsidian-telegram",
"name": "Obsidian Telegram",
"id": "telegram-bridge",
"name": "Telegram Bridge",
"version": "0.1.0",
"minAppVersion": "1.5.0",
"description": "Sync Telegram messages into Obsidian through a Supabase backend.",
"description": "Sync messages from a Telegram bot into your vault through a self-hosted Supabase backend.",
"author": "tmlnv",
"authorUrl": "https://github.com/tmlnv/obsidian-telegram",
"isDesktopOnly": false

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-telegram-plugin",
"name": "telegram-bridge-plugin",
"version": "0.1.0",
"description": "Telegram to Obsidian sync plugin backed by Supabase",
"description": "Telegram Bridge Obsidian plugin backed by Supabase",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

View file

@ -1,4 +1,4 @@
import { Notice, Plugin } from "obsidian";
import { Notice, Platform, Plugin } from "obsidian";
import {
createDefaultDistributionRule,
DEFAULT_SETTINGS,
@ -51,8 +51,10 @@ export default class ObsidianTelegramPlugin extends Plugin {
await this.saveSettings();
}
this.statusIndicator = new StatusIndicator(this);
this.statusIndicator.setIdle();
if (Platform.isDesktopApp) {
this.statusIndicator = new StatusIndicator(this);
this.statusIndicator.setIdle();
}
this.addSettingTab(new ObsidianTelegramSettingTab(this));
@ -62,20 +64,45 @@ export default class ObsidianTelegramPlugin extends Plugin {
this.addCommand({
id: "show-client-id",
name: "Show Obsidian Telegram client ID",
name: "Show client ID",
callback: () => {
this.statusIndicator?.setConnected();
console.log("Obsidian Telegram client_id:", this.settings.client_id);
new Notice(`Client ID: ${this.settings.client_id}`);
},
});
this.addCommand({
id: "sync-now",
name: "Sync Telegram messages now",
name: "Sync now",
callback: () => {
void this.manualSync();
},
});
this.addCommand({
id: "reconnect",
name: "Reconnect",
callback: () => {
void this.reinitializeSupabase().catch((error) => {
new Notice(error instanceof Error ? error.message : String(error));
});
},
});
this.addCommand({
id: "sign-out",
name: "Sign out",
checkCallback: (checking) => {
if (!this.hasSession()) {
return false;
}
if (!checking) {
void this.logout().catch((error) => {
new Notice(error instanceof Error ? error.message : String(error));
});
}
return true;
},
});
}
onunload(): void {
@ -362,7 +389,7 @@ export default class ObsidianTelegramPlugin extends Plugin {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.statusIndicator?.setError(message);
new Notice(`Obsidian Telegram initialization failed: ${message}`);
new Notice(`Telegram Bridge initialization failed: ${message}`);
}
}
@ -376,12 +403,13 @@ export default class ObsidianTelegramPlugin extends Plugin {
vaultFingerprint: this.app.vault.getName(),
pollIntervalSeconds: this.settings.poll_interval_seconds,
isRealtimeEnabled: this.settings.is_realtime_enabled,
registerInterval: (id) => this.registerInterval(id),
onMessages: async (messages) => {
await this.processMessages(messages);
},
onError: (error) => {
this.statusIndicator?.setError(error.message);
console.error("Obsidian Telegram sync failed:", error);
console.error("Telegram Bridge sync failed:", error);
},
});

View file

@ -15,7 +15,7 @@ export class ObsidianTelegramSettingTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Obsidian Telegram" });
this.renderIntro(containerEl);
this.renderConnectionHeader(containerEl);
this.renderUsageCard(containerEl);
@ -28,6 +28,22 @@ export class ObsidianTelegramSettingTab extends PluginSettingTab {
this.renderAdvancedSection(containerEl);
}
private renderIntro(containerEl: HTMLElement): void {
if (this.plugin.isSupabaseConfigured() && this.plugin.hasSession()) {
return;
}
const intro = containerEl.createDiv({ cls: "obsidian-telegram-intro" });
intro.createEl("div", {
cls: "obsidian-telegram-intro-title",
text: "Requires a self-hosted Supabase project",
});
intro.createEl("div", {
cls: "obsidian-telegram-intro-body",
text: "This plugin reads Telegram messages from your own Supabase backend. Follow the README to provision a free Supabase project, deploy the edge functions, and create a Telegram bot before connecting below.",
});
}
private createSection(
containerEl: HTMLElement,
title: string,
@ -305,10 +321,9 @@ export class ObsidianTelegramSettingTab extends PluginSettingTab {
this.plugin.settings.distribution_rules.forEach((rule, index) => {
const isFallbackRule = rule.filter_query.trim() === "{{all}}";
const ruleHeader = body.createDiv({ cls: "obsidian-telegram-rule" });
ruleHeader.createEl("h4", {
text: isFallbackRule ? `Rule ${index + 1} (fallback)` : `Rule ${index + 1}`,
});
new Setting(body)
.setName(isFallbackRule ? `Rule ${index + 1} (fallback)` : `Rule ${index + 1}`)
.setHeading();
new Setting(body)
.setName("Filter query")
@ -581,7 +596,7 @@ export class ObsidianTelegramSettingTab extends PluginSettingTab {
private renderUsageCard(containerEl: HTMLElement): void {
const sectionEl = containerEl.createDiv({ cls: "obsidian-telegram-usage-section" });
sectionEl.createEl("h3", { text: "Storage usage" });
new Setting(sectionEl).setName("Storage usage").setHeading();
const summaryEl = sectionEl.createDiv({ cls: "obsidian-telegram-usage-card" });
this.usageSummaryEl = summaryEl;
@ -706,12 +721,15 @@ export class ObsidianTelegramSettingTab extends PluginSettingTab {
scaleEl.createSpan({ text: "0%" });
scaleEl.createSpan({ text: "100%" });
const clampedUsagePercent = Math.min(100, Math.max(0, usagePercent));
const clampedThresholdPercent = Math.min(100, Math.max(0, thresholdPercent));
const trackEl = chartEl.createDiv({ cls: "obsidian-telegram-usage-track" });
const fillEl = trackEl.createDiv({ cls: "obsidian-telegram-usage-fill" });
fillEl.style.width = `${Math.min(100, Math.max(0, usagePercent))}%`;
fillEl.style.setProperty("--telegram-bridge-usage-percent", `${clampedUsagePercent}%`);
const thresholdEl = trackEl.createDiv({ cls: "obsidian-telegram-usage-threshold" });
thresholdEl.style.left = `${Math.min(100, Math.max(0, thresholdPercent))}%`;
thresholdEl.style.setProperty("--telegram-bridge-threshold-percent", `${clampedThresholdPercent}%`);
thresholdEl.setAttribute("aria-label", `Warning threshold ${thresholdPercent}%`);
const thresholdLabelEl = chartEl.createDiv({ cls: "obsidian-telegram-usage-threshold-label" });

View file

@ -10,6 +10,7 @@ export interface SyncEngineOptions {
vaultFingerprint: string;
pollIntervalSeconds: number;
isRealtimeEnabled: boolean;
registerInterval: (id: number) => number;
onMessages: (messages: MessageRow[]) => Promise<void>;
onError: (error: Error) => void;
}
@ -23,6 +24,7 @@ export class SyncEngine {
private readonly onError: (error: Error) => void;
private readonly pollIntervalSeconds: number;
private readonly isRealtimeEnabled: boolean;
private readonly registerInterval: (id: number) => number;
private pollIntervalId: number | null = null;
private realtimeChannel: RealtimeChannel | null = null;
private isPolling = false;
@ -38,6 +40,7 @@ export class SyncEngine {
this.vaultFingerprint = options.vaultFingerprint;
this.pollIntervalSeconds = options.pollIntervalSeconds;
this.isRealtimeEnabled = options.isRealtimeEnabled;
this.registerInterval = options.registerInterval;
this.onMessages = options.onMessages;
this.onError = options.onError;
}
@ -47,9 +50,11 @@ export class SyncEngine {
await this.poll();
this.stopPolling();
this.pollIntervalId = window.setInterval(() => {
void this.poll();
}, this.pollIntervalSeconds * 1000);
this.pollIntervalId = this.registerInterval(
window.setInterval(() => {
void this.poll();
}, this.pollIntervalSeconds * 1000),
);
if (this.isRealtimeEnabled) {
this.subscribeRealtime();

View file

@ -4,6 +4,26 @@
gap: 6px;
}
.obsidian-telegram-intro {
margin: 0 0 16px;
padding: 12px 14px;
border: 1px solid var(--background-modifier-border);
background: color-mix(in srgb, var(--interactive-accent) 10%, var(--background-secondary));
border-radius: 10px;
}
.obsidian-telegram-intro-title {
color: var(--text-normal);
font-weight: 600;
margin-bottom: 4px;
}
.obsidian-telegram-intro-body {
color: var(--text-muted);
font-size: 0.9rem;
line-height: 1.4;
}
.obsidian-telegram-settings-header {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@ -128,6 +148,7 @@
.obsidian-telegram-usage-fill {
position: absolute;
inset: 0 auto 0 0;
width: var(--telegram-bridge-usage-percent, 0%);
max-width: 100%;
border-radius: inherit;
background: linear-gradient(90deg, color-mix(in srgb, var(--interactive-accent) 70%, white), var(--interactive-accent));
@ -137,6 +158,7 @@
position: absolute;
top: -3px;
bottom: -3px;
left: var(--telegram-bridge-threshold-percent, 0%);
width: 2px;
margin-left: -1px;
background: var(--text-normal);

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.1.0": "1.5.0"
}