refactor: move telegram poll visibility out of core

This commit is contained in:
Peter Steinberger
2026-04-27 12:25:51 +01:00
parent 3bc29dd604
commit 7363fb4a44
3 changed files with 69 additions and 27 deletions

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { resolveTelegramPollVisibility } from "./poll-visibility.js";
describe("telegram poll visibility", () => {
it("resolves poll visibility aliases", () => {
expect(resolveTelegramPollVisibility({ pollAnonymous: true })).toBe(true);
expect(resolveTelegramPollVisibility({ pollPublic: true })).toBe(false);
expect(resolveTelegramPollVisibility({})).toBeUndefined();
expect(() => resolveTelegramPollVisibility({ pollAnonymous: true, pollPublic: true })).toThrow(
/mutually exclusive/i,
);
});
});

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { hasPollCreationParams, resolveTelegramPollVisibility } from "./poll-params.js";
import { hasPollCreationParams } from "./poll-params.js";
describe("poll params", () => {
it("does not treat explicit false booleans as poll creation params", () => {
@@ -60,12 +60,9 @@ describe("poll params", () => {
expect(hasPollCreationParams({ poll_public: "true" })).toBe(true);
});
it("resolves telegram poll visibility flags", () => {
expect(resolveTelegramPollVisibility({ pollAnonymous: true })).toBe(true);
expect(resolveTelegramPollVisibility({ pollPublic: true })).toBe(false);
expect(resolveTelegramPollVisibility({})).toBeUndefined();
expect(() => resolveTelegramPollVisibility({ pollAnonymous: true, pollPublic: true })).toThrow(
/mutually exclusive/i,
);
it("ignores poll vote params when deciding whether send should become poll", () => {
expect(hasPollCreationParams({ pollId: "poll-1" })).toBe(false);
expect(hasPollCreationParams({ pollOptionId: "answer-1" })).toBe(false);
expect(hasPollCreationParams({ pollOptionIndexes: [1] })).toBe(false);
});
});

View File

@@ -14,40 +14,67 @@ const SHARED_POLL_CREATION_PARAM_DEFS = {
pollMulti: { kind: "boolean" },
} satisfies Record<string, PollCreationParamDef>;
const TELEGRAM_POLL_CREATION_PARAM_DEFS = {
pollDurationSeconds: { kind: "number" },
pollAnonymous: { kind: "boolean" },
pollPublic: { kind: "boolean" },
} satisfies Record<string, PollCreationParamDef>;
export const POLL_CREATION_PARAM_DEFS: Record<string, PollCreationParamDef> = {
...SHARED_POLL_CREATION_PARAM_DEFS,
...TELEGRAM_POLL_CREATION_PARAM_DEFS,
};
export const POLL_CREATION_PARAM_DEFS: Record<string, PollCreationParamDef> =
SHARED_POLL_CREATION_PARAM_DEFS;
type SharedPollCreationParamName = keyof typeof SHARED_POLL_CREATION_PARAM_DEFS;
const POLL_CREATION_PARAM_NAMES = Object.keys(POLL_CREATION_PARAM_DEFS);
export const SHARED_POLL_CREATION_PARAM_NAMES = Object.keys(
SHARED_POLL_CREATION_PARAM_DEFS,
) as SharedPollCreationParamName[];
const SHARED_POLL_CREATION_PARAM_KEY_SET = new Set(
SHARED_POLL_CREATION_PARAM_NAMES.map(normalizePollParamKey),
);
const POLL_VOTE_PARAM_KEY_SET = new Set(
["pollId", "pollOptionId", "pollOptionIds", "pollOptionIndex", "pollOptionIndexes"].map(
normalizePollParamKey,
),
);
function readPollParamRaw(params: Record<string, unknown>, key: string): unknown {
return readSnakeCaseParamRaw(params, key);
}
export function resolveTelegramPollVisibility(params: {
pollAnonymous?: boolean;
pollPublic?: boolean;
}): boolean | undefined {
if (params.pollAnonymous && params.pollPublic) {
throw new Error("pollAnonymous and pollPublic are mutually exclusive");
function normalizePollParamKey(key: string): string {
return normalizeLowercaseStringOrEmpty(key.replaceAll("_", ""));
}
function isChannelPollCreationParamName(key: string): boolean {
const normalized = normalizePollParamKey(key);
return (
normalized.startsWith("poll") &&
!SHARED_POLL_CREATION_PARAM_KEY_SET.has(normalized) &&
!POLL_VOTE_PARAM_KEY_SET.has(normalized)
);
}
function hasExplicitUnknownPollValue(key: string, value: unknown): boolean {
if (value === true) {
return true;
}
return params.pollAnonymous ? true : params.pollPublic ? false : undefined;
if (typeof value === "number") {
return Number.isFinite(value) && value !== 0;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length === 0) {
return false;
}
if (normalizePollParamKey(key).includes("duration")) {
const parsed = Number(trimmed);
return Number.isFinite(parsed) && parsed !== 0;
}
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
return normalized !== "false" && normalized !== "0";
}
if (Array.isArray(value)) {
return value.some((entry) => hasExplicitUnknownPollValue(key, entry));
}
return false;
}
export function hasPollCreationParams(params: Record<string, unknown>): boolean {
for (const key of POLL_CREATION_PARAM_NAMES) {
for (const key of SHARED_POLL_CREATION_PARAM_NAMES) {
const def = POLL_CREATION_PARAM_DEFS[key];
const value = readPollParamRaw(params, key);
if (def.kind === "string" && typeof value === "string" && value.trim().length > 0) {
@@ -88,5 +115,10 @@ export function hasPollCreationParams(params: Record<string, unknown>): boolean
}
}
}
for (const [key, value] of Object.entries(params)) {
if (isChannelPollCreationParamName(key) && hasExplicitUnknownPollValue(key, value)) {
return true;
}
}
return false;
}