fix(slack): channel is not supported in combination with webhooks (#770)
#766
This commit is contained in:
parent
4872029b87
commit
595a29056d
14 changed files with 21 additions and 44 deletions
|
|
@ -32,7 +32,14 @@ export const createClient = (config: Config = {}): Client => {
|
|||
|
||||
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
|
||||
|
||||
const beforeRequest = async (options: RequestOptions) => {
|
||||
const beforeRequest = async <
|
||||
TData = unknown,
|
||||
TResponseStyle extends 'data' | 'fields' = 'fields',
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
>(
|
||||
options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
|
||||
) => {
|
||||
const opts = {
|
||||
..._config,
|
||||
...options,
|
||||
|
|
@ -61,13 +68,14 @@ export const createClient = (config: Config = {}): Client => {
|
|||
opts.headers.delete('Content-Type');
|
||||
}
|
||||
|
||||
const url = buildUrl(opts);
|
||||
const resolvedOpts = opts as typeof opts &
|
||||
ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
|
||||
const url = buildUrl(resolvedOpts);
|
||||
|
||||
return { opts, url };
|
||||
return { opts: resolvedOpts, url };
|
||||
};
|
||||
|
||||
const request: Client['request'] = async (options) => {
|
||||
// @ts-expect-error
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
const requestInit: ReqInit = {
|
||||
redirect: 'follow',
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ type SseFn = <
|
|||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = 'fields',
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
||||
options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||
|
||||
type RequestFn = <
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ export const mergeHeaders = (
|
|||
mergedHeaders.append(key, v as string);
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
// assume object headers are meant to be JSON stringified, i.e. their
|
||||
// assume object headers are meant to be JSON stringified, i.e., their
|
||||
// content value in OpenAPI specification is 'application/json'
|
||||
mergedHeaders.set(
|
||||
key,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export interface Config {
|
|||
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||
/**
|
||||
* A function transforming response data before it's returned. This is useful
|
||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
||||
* for post-processing data, e.g., converting ISO strings into Date objects.
|
||||
*/
|
||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export function getValidRequestBody(options: {
|
|||
return hasSerializedBody ? options.serializedBody : null;
|
||||
}
|
||||
|
||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
||||
// not all clients implement a serializedBody property (i.e., client-axios)
|
||||
return options.body !== '' ? options.body : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponse, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown> = Options2<TData, ThrowOnError, TResponse> & {
|
||||
/**
|
||||
* You can provide a client instance returned by `createClient()` instead of
|
||||
* individual options. This might be also useful if you want to implement a
|
||||
|
|
@ -307,7 +307,7 @@ export const tagSnapshots = <ThrowOnError extends boolean = false>(options: Opti
|
|||
/**
|
||||
* Execute a restic command against a repository (dev panel only)
|
||||
*/
|
||||
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError>) => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
|
||||
export const devPanelExec = <ThrowOnError extends boolean = false>(options: Options<DevPanelExecData, ThrowOnError, DevPanelExecResponse>) => (options.client ?? client).sse.post<DevPanelExecResponses, DevPanelExecErrors, ThrowOnError>({
|
||||
url: '/api/v1/repositories/{shortId}/exec',
|
||||
...options,
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -3526,7 +3526,6 @@ export type GetScheduleNotificationsResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -3628,7 +3627,6 @@ export type UpdateScheduleNotificationsResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4203,7 +4201,6 @@ export type ListNotificationDestinationsResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4273,7 +4270,6 @@ export type CreateNotificationDestinationData = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4348,7 +4344,6 @@ export type CreateNotificationDestinationResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4467,7 +4462,6 @@ export type GetNotificationDestinationResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4538,7 +4532,6 @@ export type UpdateNotificationDestinationData = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
@ -4622,7 +4615,6 @@ export type UpdateNotificationDestinationResponses = {
|
|||
} | {
|
||||
type: 'slack';
|
||||
webhookUrl: string;
|
||||
channel?: string;
|
||||
username?: string;
|
||||
iconEmoji?: string;
|
||||
} | {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ const defaultValuesForType = {
|
|||
slack: {
|
||||
type: "slack" as const,
|
||||
webhookUrl: "",
|
||||
channel: "",
|
||||
username: "",
|
||||
iconEmoji: "",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,20 +27,6 @@ export const SlackForm = ({ form }: Props) => {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="#backups" />
|
||||
</FormControl>
|
||||
<FormDescription>Override the default channel (use # for channels, @ for users).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ function NotificationConfigRows({ config }: { config: NotificationConfig }) {
|
|||
return (
|
||||
<>
|
||||
<ConfigRow icon={<Globe className="h-4 w-4" />} label="Webhook URL" value={config.webhookUrl} mono />
|
||||
<ConfigRow icon={<Hash className="h-4 w-4" />} label="Channel" value={config.channel || "—"} />
|
||||
<ConfigRow icon={<Bot className="h-4 w-4" />} label="Bot Username" value={config.username || "—"} />
|
||||
<ConfigRow icon={<Smile className="h-4 w-4" />} label="Icon Emoji" value={config.iconEmoji || "—"} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export const emailNotificationConfigSchema = z.object({
|
|||
export const slackNotificationConfigSchema = z.object({
|
||||
type: z.literal("slack"),
|
||||
webhookUrl: z.string().min(1),
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
iconEmoji: z.string().optional(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ describe("notification shoutrrr URL builders", () => {
|
|||
).toBe("pushover://shoutrrr:api-token@user-key/?devices=iphone%2Cipad&priority=1");
|
||||
});
|
||||
|
||||
test("builds slack URLs and rejects invalid webhook formats", () => {
|
||||
test("builds upstream-compatible slack webhook URLs and rejects invalid webhook formats", () => {
|
||||
expect(
|
||||
buildSlackShoutrrrUrl({
|
||||
type: "slack",
|
||||
|
|
@ -164,11 +164,10 @@ describe("notification shoutrrr URL builders", () => {
|
|||
buildSlackShoutrrrUrl({
|
||||
type: "slack",
|
||||
webhookUrl: "https://hooks.slack.com/services/T000/B000/XXX",
|
||||
channel: "#alerts",
|
||||
username: "Alert Bot",
|
||||
iconEmoji: ":robot_face:",
|
||||
}),
|
||||
).toBe("slack://hook:T000-B000-XXX@webhook?channel=%23alerts&username=Alert+Bot&icon_emoji=%3Arobot_face%3A");
|
||||
).toBe("slack://hook:T000-B000-XXX@webhook?username=Alert+Bot&icon_emoji=%3Arobot_face%3A");
|
||||
|
||||
expect(() =>
|
||||
buildSlackShoutrrrUrl({
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ describe("mapNotificationConfigSecrets", () => {
|
|||
{
|
||||
type: "slack",
|
||||
webhookUrl: "https://hooks.slack.test/services/a/b/c",
|
||||
channel: "#alerts",
|
||||
username: "zerobyte",
|
||||
iconEmoji: ":wave:",
|
||||
},
|
||||
|
|
@ -17,7 +16,6 @@ describe("mapNotificationConfigSecrets", () => {
|
|||
expect(transformed).toEqual({
|
||||
type: "slack",
|
||||
webhookUrl: "sealed:https://hooks.slack.test/services/a/b/c",
|
||||
channel: "#alerts",
|
||||
username: "zerobyte",
|
||||
iconEmoji: ":wave:",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ export const buildSlackShoutrrrUrl = (config: Extract<NotificationConfig, { type
|
|||
shoutrrrUrl.password = `${tokenA}-${tokenB}-${tokenC}`;
|
||||
shoutrrrUrl.hostname = "webhook";
|
||||
|
||||
if (config.channel) {
|
||||
shoutrrrUrl.searchParams.append("channel", config.channel);
|
||||
}
|
||||
if (config.username) {
|
||||
shoutrrrUrl.searchParams.append("username", config.username);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue