Messenger
Sending the core's messages through a connector — send, edit, delete and sendFile, the Message and Recipient objects, and everything the core checks before your port is called.
Subscriby\Connector\Contracts\Ports\Messenger. Bound by messaging; broadcasts adds bulk sends on the same class.
The core composes every message once and reads the result rather than trusting an HTTP status, because platforms refuse with 200 OK as often as with an exception. Your Messenger renders nothing: it lays the body your TextRenderer produced and the actions out in the platform's shape and reports what happened.
Methods
| Method | Called when | Returns |
|---|---|---|
send(installation, credentials, Recipient, Message) | Every confirmation, reminder, alert, sale notice, pass notice and broadcast the core delivers to one person. | DeliveryResult |
sendFile(installation, credentials, recipient, url, ?caption) | A file the core has to hand a person: an access-code export, a receipt. Refused by the core when the manifest says supports_files: false. | DeliveryResult |
The message
new Message(
body: '<b>Payment received.</b> Your access to <i>Signals</i> is active.',
actions: [
MessageAction::url('Open the channel', 'https://…'),
MessageAction::command('Manage plan', ManagementCommand::PlanManage, ['plan' => $id]),
],
meta: ['example' => ['silent' => true]],
);bodyis the canonical HTML subset; hand it to yourTextRenderer.actionsare built through four factories only:url(label, url),callback(label, data),copy(label, text)andcommand(label, command, params). A command button names a catalogue command in the core's words and each connector renders it into the callback its own surface answers to, so no core message spells a platform's routing.MessageAction::assertWithin()has already thrown on the developer's machine for a callback the platform would truncate.metais keyed by connector:$message->metaFor('example')gives you your own extras and every other connector ignores them.
The recipient is a Recipient: the IdentityRef to reach and an optional threadId when the platform threads conversations.
What the core has done before calling you
- The capability switch. A creator may switch
messagingorbroadcastsoff for one installation; the core answers aConfigurationfailure itself ("The messaging capability is switched off for this installation") and never calls the port. - The limits. A body over
max_lengthhas been cut into parts, each within the limit: the core splits at the last paragraph break that fits, then the last line break, then the last space, never inside a tag or an entity, and closes and reopens the tags open at a cut so every part is well-formed canonical HTML. Yoursend()receives the parts in order, one call each, and the buttons arrive with the last part; the core reports the last part's result, or the first failure, to the caller.Message::assertWithin($manifest->messaging)has run on every part; a message with too many buttons or too many per row, or a limit too short to carry a word beside the message's own markup, becomes aConfigurationfailure before your port. - Files.
sendFile()is refused by the core withConfigurationwhen the manifest sayssupports_files: false. - Reach. The core chose which account to write to, by the notice's intent: a grant notice goes to the connector of the grant only, a support reply to the connector the member wrote from, and every other notice to the member's preferred account and the accounts they switched notices on for; an account your
IdentityResolver::deliveryTarget()answered null for is passed over for the next, and email carries an account-critical notice once no account did. A creator's alerts follow their alert destinations. You are never asked which account; you are handed one. - Pacing. Bulk sends are spaced by
pacing; the broadcast pipeline readsmin_interval_microseconds, jobs that send several messages to one person sleepper_recipient_interval_microseconds. - Accounting. Every call is timed and counted per connector and operation, and its
DeliveryResultis read: afailureof a retryable kind requeues the job, a creator-actionable kind becomes a health verdict, anything else is recorded as failed.
What you return
DeliveryResult::delivered($platformMessageId);
DeliveryResult::failed($this->failures->classify($response));externalMessageId is what edit() and delete() will be given back, so return the platform's real id. Build every failure through your FailureClassifier; never throw for a platform refusal.
How Telegram does it
The canonical subset is Telegram's own parse mode, so the body goes as written; the actions become one inline button per row, the layout every core message has always used; callback data crosses verbatim as the key:value pairs the bot's handler already parses, and a command button is rendered into those pairs by the connector's CommandCallbacks. The result is read from the response body, because Telegram refuses inside a 200 OK.
What the kit checks
No kit rule drives Messenger directly, because a send needs a platform. Subscriby's suite sends every core message class through the fake connector's FakeMessenger, which records what it was asked and asserts with assertSentTo($externalId, $containing), assertNotSentTo($externalId, $containing) and assertNothingSent(); use the same fake to test the core paths your connector relies on, and your platform's fake client to test the port itself.
Silence is a failure
A Messenger that swallows a refusal and returns delivered teaches the core that a member was told something they never received. Classify and return; the core knows what to do with each kind.
How is this guide?
UiSlots
The connector's contributions to the core's UI slots — the eighteen slots and what each renders, the SlotContribution and SlotContext objects, the placeholder rule, and what a contribution may read.
AccessController
Giving and taking away access to a place — grant, revoke, revokeReference, admit, membership, announce and reconcile, the idempotency each must keep, and every core path that calls them.