feat: add Mattermost channel support
Add Mattermost as a supported messaging channel with bot API and WebSocket integration. Includes channel state tracking (tint, summary, details), multi-account support, and delivery target routing. Update documentation and tests to include Mattermost alongside existing channels.
This commit is contained in:
@@ -164,6 +164,39 @@ export type SlackStatus = {
|
||||
lastProbeAt?: number | null;
|
||||
};
|
||||
|
||||
export type MattermostBot = {
|
||||
id?: string | null;
|
||||
username?: string | null;
|
||||
};
|
||||
|
||||
export type MattermostProbe = {
|
||||
ok: boolean;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
elapsedMs?: number | null;
|
||||
bot?: MattermostBot | null;
|
||||
};
|
||||
|
||||
export type MattermostStatus = {
|
||||
configured: boolean;
|
||||
botTokenSource?: string | null;
|
||||
running: boolean;
|
||||
connected?: boolean | null;
|
||||
lastConnectedAt?: number | null;
|
||||
lastDisconnect?: {
|
||||
at: number;
|
||||
status?: number | null;
|
||||
error?: string | null;
|
||||
loggedOut?: boolean | null;
|
||||
} | null;
|
||||
lastStartAt?: number | null;
|
||||
lastStopAt?: number | null;
|
||||
lastError?: string | null;
|
||||
baseUrl?: string | null;
|
||||
probe?: MattermostProbe | null;
|
||||
lastProbeAt?: number | null;
|
||||
};
|
||||
|
||||
export type SignalProbe = {
|
||||
ok: boolean;
|
||||
status?: number | null;
|
||||
@@ -363,6 +396,7 @@ export type CronPayload =
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "mattermost"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
|
||||
70
ui/src/ui/views/channels.mattermost.ts
Normal file
70
ui/src/ui/views/channels.mattermost.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
import { formatAgo } from "../format";
|
||||
import type { MattermostStatus } from "../types";
|
||||
import type { ChannelsProps } from "./channels.types";
|
||||
import { renderChannelConfigSection } from "./channels.config";
|
||||
|
||||
export function renderMattermostCard(params: {
|
||||
props: ChannelsProps;
|
||||
mattermost?: MattermostStatus | null;
|
||||
accountCountLabel: unknown;
|
||||
}) {
|
||||
const { props, mattermost, accountCountLabel } = params;
|
||||
|
||||
return html`
|
||||
<div class="card">
|
||||
<div class="card-title">Mattermost</div>
|
||||
<div class="card-sub">Bot token + WebSocket status and configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
<span>${mattermost?.configured ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Running</span>
|
||||
<span>${mattermost?.running ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Connected</span>
|
||||
<span>${mattermost?.connected ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Base URL</span>
|
||||
<span>${mattermost?.baseUrl || "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${mattermost?.lastStartAt ? formatAgo(mattermost.lastStartAt) : "n/a"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Last probe</span>
|
||||
<span>${mattermost?.lastProbeAt ? formatAgo(mattermost.lastProbeAt) : "n/a"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${mattermost?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${mattermost.lastError}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
${mattermost?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${mattermost.probe.ok ? "ok" : "failed"} -
|
||||
${mattermost.probe.status ?? ""} ${mattermost.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "mattermost", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ChannelsStatusSnapshot,
|
||||
DiscordStatus,
|
||||
IMessageStatus,
|
||||
MattermostStatus,
|
||||
NostrProfile,
|
||||
NostrStatus,
|
||||
SignalStatus,
|
||||
@@ -23,6 +24,7 @@ import { channelEnabled, renderChannelAccountCount } from "./channels.shared";
|
||||
import { renderChannelConfigSection } from "./channels.config";
|
||||
import { renderDiscordCard } from "./channels.discord";
|
||||
import { renderIMessageCard } from "./channels.imessage";
|
||||
import { renderMattermostCard } from "./channels.mattermost";
|
||||
import { renderNostrCard } from "./channels.nostr";
|
||||
import { renderSignalCard } from "./channels.signal";
|
||||
import { renderSlackCard } from "./channels.slack";
|
||||
@@ -39,6 +41,7 @@ export function renderChannels(props: ChannelsProps) {
|
||||
| undefined;
|
||||
const discord = (channels?.discord ?? null) as DiscordStatus | null;
|
||||
const slack = (channels?.slack ?? null) as SlackStatus | null;
|
||||
const mattermost = (channels?.mattermost ?? null) as MattermostStatus | null;
|
||||
const signal = (channels?.signal ?? null) as SignalStatus | null;
|
||||
const imessage = (channels?.imessage ?? null) as IMessageStatus | null;
|
||||
const nostr = (channels?.nostr ?? null) as NostrStatus | null;
|
||||
@@ -62,6 +65,7 @@ export function renderChannels(props: ChannelsProps) {
|
||||
telegram,
|
||||
discord,
|
||||
slack,
|
||||
mattermost,
|
||||
signal,
|
||||
imessage,
|
||||
nostr,
|
||||
@@ -97,7 +101,7 @@ function resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKe
|
||||
if (snapshot?.channelOrder?.length) {
|
||||
return snapshot.channelOrder;
|
||||
}
|
||||
return ["whatsapp", "telegram", "discord", "slack", "signal", "imessage", "nostr"];
|
||||
return ["whatsapp", "telegram", "discord", "slack", "mattermost", "signal", "imessage", "nostr"];
|
||||
}
|
||||
|
||||
function renderChannel(
|
||||
@@ -135,6 +139,12 @@ function renderChannel(
|
||||
slack: data.slack,
|
||||
accountCountLabel,
|
||||
});
|
||||
case "mattermost":
|
||||
return renderMattermostCard({
|
||||
props,
|
||||
mattermost: data.mattermost,
|
||||
accountCountLabel,
|
||||
});
|
||||
case "signal":
|
||||
return renderSignalCard({
|
||||
props,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
ConfigUiHints,
|
||||
DiscordStatus,
|
||||
IMessageStatus,
|
||||
MattermostStatus,
|
||||
NostrProfile,
|
||||
NostrStatus,
|
||||
SignalStatus,
|
||||
@@ -53,6 +54,7 @@ export type ChannelsChannelData = {
|
||||
telegram?: TelegramStatus;
|
||||
discord?: DiscordStatus | null;
|
||||
slack?: SlackStatus | null;
|
||||
mattermost?: MattermostStatus | null;
|
||||
signal?: SignalStatus | null;
|
||||
imessage?: IMessageStatus | null;
|
||||
nostr?: NostrStatus | null;
|
||||
|
||||
Reference in New Issue
Block a user