Connect a channel by issuing a one-time code the user posts as /connect <code>
in their channel. A secret-token-guarded webhook matches the code, links the
channel as a SocialAccount (chat_id in meta), and records it on the request so
the connect endpoint can poll for completion. Adds the TelegramConnectRequest
model + migration, the connect/status endpoints, the public webhook route (CSRF
exempt), a ConnectionVerifier branch (getChat liveness), and a telegram:set-webhook
command. Tests cover the code issue, webhook link, secret rejection, expired/
unknown codes, status polling, and the command.
Register Telegram as a platform: Platform/ContentType enum cases, a
platforms.telegram config block (shared bot token via env), TelegramPublisher
(sendMessage / sendPhoto|Video|Document / sendMediaGroup over the Bot API, HTML
parse mode, 4096 limit with long text split off a 1024 caption), wired into the
publisher dispatch. Add a Telegram ContentSanitizer branch (Telegram-allowed
HTML + ampersand escaping), MediaOptimizer/profile-url/factory support, and the
TelegramPublishException. Tests cover text, single media, album, long-text split,
overflow, API errors, private-channel URLs, and sanitization.
Add a test that a network error during handle resolution degrades the mention
to plain text instead of failing the post (exercises the resolveHandleToDid
try/catch), and a test asserting the post URL is built from the configured
web_app host (a revert to a hardcoded host would now fail).
Add a unit test asserting each BlueskyLexicon constant equals its AT Protocol
NSID — a wrong value (not just a typo'd name) would otherwise fail silently at
runtime. Add a test that a URL with a matching '(' keeps its trailing ')'
(Wikipedia-style), covering the previously-untested keep branch of the trim.
The Bluesky lexicon identifiers (createRecord, createSession, feed.post, facet
types, etc.) were repeated as magic strings across BlueskyPublisher,
BlueskyAnalytics, ConnectionVerifier and BlueskyController, where a typo fails
silently at runtime as "Invalid request". Define them once as named constants
so a typo is an undefined-constant error instead. Tests keep the literal NSIDs
as the independent contract.
The image tests only checked that createRecord was sent — the faked empty
download meant no blob was ever attached, so they never verified the embed.
Drive a real (mocked) optimize + upload and assert the createRecord record
carries an app.bsky.embed.images embed, and that six images upload exactly four.
Import Exception instead of referencing it inline, read the blob via data_get,
source the web-app host from config (new bluesky.web_app key) instead of
hardcoding bsky.app, and drop the getUtf8ByteOffset no-op — the PREG /u offset
is already in bytes, so the call sites use it directly.
The URL regex captured trailing sentence punctuation and an unmatched closing
paren ("see https://x.com).") into the link facet, so Bluesky rendered the
punctuation as part of a malformed link. Trim it like the official atproto
tokenizer. Add tests for the trim and for byte-offset correctness after
multibyte characters (emoji/accents).
resolveHandle is a public read served by the AppView, so drop the PDS-first
auth, the multi-endpoint fallback loop and the threaded service/account args in
favor of one unauthenticated GET against config public_appview — matching how
BlueskyAnalytics already reads from the AppView. The try/catch stays so a
network error degrades the mention to plain text instead of failing the post.
Remove the now-obsolete fallback test.
Memoize by key (array_key_exists) so a handle that fails to resolve is tried
once per post instead of on every repeat occurrence, and add a test that a
repeated handle resolves once. Cast the PREG_OFFSET_CAPTURE offsets to int to
match getUtf8ByteOffset's signature, and reword the resolver docblock to say
"configured default service" instead of naming bsky.social.
Add tests for the two untested branches of the DID resolution: falling back to
the public AppView when the account's PDS can't resolve a handle, and a post
mixing a resolvable mention (kept as a facet) with an unresolvable one (a 200
without a DID, left as plain text). The prior tests only hit the happy path and
the all-endpoints-fail path via a wildcard that masked the fallback.
Remove the final Log::warning (and the lastError/lastEndpoint tracking that only
fed it) — the per-endpoint debug log already covers diagnosis. Fake the
createRecord endpoint from config('trypost.platforms.bluesky.default_service')
in the new tests instead of hardcoding the host.
Use the existing config('trypost.platforms.bluesky.public_appview') and
default_service keys for the handle-resolution fallbacks instead of hardcoding
the hosts, so a self-hosted override (BLUESKY_PUBLIC_APPVIEW) is honored here
like it already is in BlueskyAnalytics. Import Throwable instead of referencing
it inline.