Notify Telegram
Telegram notifications for WordPress events
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/wpseed/notify-telegram/archive/refs/heads/main.zipNotifications for WordPress events, delivered to the channels you configure: Telegram, email or any
incoming webhook. Scaffolded from the lab's starter-plugin template: namespace
Wpseed\NotifyTelegram, text domain notify-telegram, package wpseed/notify-telegram.
What is inside
| File / directory | Purpose |
|---|---|
notify-telegram.php |
Plugin header, autoloader include, Plugin::boot() |
src/Plugin.php |
Singleton entry point: builds the objects, registers the hooks, activation |
src/Message.php |
The text that leaves the site: %placeholder% rendering and validation |
src/Settings/Settings.php |
The single option: switches, channel values, message templates |
src/Channel/ |
Channel interface, Result, ChannelRegistry, TelegramChannel, EmailChannel, WebhookChannel |
src/Event/ |
Event, EventRegistry, EventSource, UserEvents (registration, failed login), CommentEvents |
src/Delivery/ |
Router (what goes out), Queue (WP-Cron, retries), Log (last 20 attempts) |
src/Admin/AdminPage.php |
The single menu entry and the React boot behind it: Events and Settings are tabs on that one screen |
src/Rest/SettingsController.php |
The routes the screen reads and writes through: /settings, /test, /log |
src/Delivery/TestSender.php |
The test button: one message through every configured channel |
admin-ui/, package.json, vite.config.mjs |
React + antd sources and the Vite build of the screen |
bin/build |
Archive builder (composer build): a copy without the development files + zip into dist/ |
.github/workflows/build-plugin.yml |
The same build in CI, triggered by a version tag |
.github/workflows/php-floor.yml |
The PHP floor check: install and lint on 8.2, on every push |
readme.txt |
The listing in the plugin directory: header, description, FAQ, changelog — checked with the directory's own readme validator |
tests/unit/ |
Fast unit tests (PHPUnit, WordPress is not loaded) |
tests/integration/ |
Integration tests (WP_UnitTestCase, real WordPress + MySQL) |
Setup and commands
cd web/app/plugins/notify-telegram
composer install # development dependencies (phpunit, wp-phpunit, WPCS)
composer test # every test
composer test:unit # unit only
composer test:integration # integration only (boots WordPress)
composer lint # phpcs (WordPress Coding Standards)
composer lint:fix # phpcbf, auto-fix
composer build # shippable archive: dist/notify-telegram/ + dist/notify-telegram.zip
npm install # front-end dependencies (React, antd, Vite)
npm run build # bundle the admin screen into assets/admin/
npm run watch # same, rebuilding on every change
Events, channels and delivery
WordPress hook → EventSource (declares the event, fills its placeholders)
→ Router (master switch, event toggle, channels that are enabled and configured)
→ Queue (one WP-Cron event per channel, retries, log)
→ Channel (Telegram / email / webhook)
Adding a channel is one class: implement Channel (id, label, fields, is_configured, send)
and add it to the array in Plugin::create_channels(), or hand it in through the
notify_telegram_channels filter. fields() is what the settings screen renders, so a channel brings
its own form. Adding an event is one class too: implement EventSource and register it through the
notify_telegram_event_sources filter — the placeholders an event declares are both its documentation
and the validation rules for its message template.
The screen
Notify Telegram → admin.php?page=notify-telegram Events (default)
admin.php?page=notify-telegram&tab=settings Settings
One menu entry, no submenu: Events and Settings are tabs inside the page. WordPress prints the
submenu list only when $submenu holds entries for the parent slug (wp-admin/menu-header.php), so the
plugin registers a single add_menu_page() and no add_submenu_page() at all — not even one that
reuses the parent slug, which is the usual trick for labelling the first entry and is exactly what
unfolds the list on hover. The sidebar shows one plain link, and toplevel_page_notify-telegram is the
only screen the bundle is enqueued on.
Events lists every registered event with its switch, its message and the placeholders that event accepts; Settings holds the master switch, one card per channel with the fields the channel itself declares, the test button and the last twenty delivery attempts. The state lives in the root component, so switching tabs never loses an edit and one Save button stores both — the plugin keeps all of it in a single option anyway.
The open tab is addressable: AdminPage::initial_tab() puts the requested tab query argument (only
events or settings pass; anything else falls back to the first tab) into the configuration the screen
is booted with, and the application writes the chosen tab back into the address bar with
history.replaceState(). A reload or a shared link therefore opens the same tab, without a WordPress
page per tab.
The menu entry
One entry, and AdminPage::MENU_POSITION is the only knob for where it sits: 75.9 gives it its own
slot under Tools (75) and above Settings (80), while core's other sections are at 4 (a separator),
10 (Media), 20 (Pages), 60 (Appearance), 65 (Plugins) and 70 (Users) — a plugin that passes no position
at all lands after all of them. Move the number to move the entry: 3 is directly under Dashboard, 59.9
just above Appearance. A position another plugin has already taken costs nothing: core checks the key
before writing and gives the later registration a small offset instead of a shared slot, which is why
positions live in the menu as string keys and why a float is a legitimate value here.
The icon is the core bell (dashicons-bell), so the entry is painted by the admin colour scheme exactly
like the rest of the sidebar — including the dimmed state, the hover colour and the highlight of the open
screen, none of which an image of our own would follow. AdminPage::ICON is the one place to change it:
any dashicons-* class works, and WordPress renders it through div.wp-menu-image:before
(wp-admin/menu-header.php). An icon of our own is possible — a data:image/svg+xml;base64,… URI is the
one form core special-cases, and it keeps whatever palette the SVG declares — but that icon then ignores
the colour scheme entirely, so it is not what ships here.
PHP prints the WordPress heading, the description and the mount element; the application fills the mount
element and nothing else. The lab's starter-plugin keeps a working reference of the build setup —
including the part that is easy to get wrong, that the bundle must be an IIFE, because
wp_enqueue_script() prints a classic `