Merge pull request #7 from ksyasuda/feature/add-anilist-tracking

Add AniList Tracking
This commit is contained in:
2026-02-17 00:08:33 -08:00
committed by GitHub
19 changed files with 1642 additions and 19 deletions

View File

@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
isAllowedAnilistExternalUrl,
isAllowedAnilistSetupNavigationUrl,
} from "./anilist-url-guard";
test("allows only AniList https URLs for external opens", () => {
assert.equal(isAllowedAnilistExternalUrl("https://anilist.co"), true);
assert.equal(
isAllowedAnilistExternalUrl("https://www.anilist.co/settings/developer"),
true,
);
assert.equal(isAllowedAnilistExternalUrl("http://anilist.co"), false);
assert.equal(isAllowedAnilistExternalUrl("https://example.com"), false);
assert.equal(isAllowedAnilistExternalUrl("file:///tmp/test"), false);
assert.equal(isAllowedAnilistExternalUrl("not a url"), false);
});
test("allows only AniList https or data URLs for setup navigation", () => {
assert.equal(
isAllowedAnilistSetupNavigationUrl("https://anilist.co/api/v2/oauth/authorize"),
true,
);
assert.equal(
isAllowedAnilistSetupNavigationUrl(
"data:text/html;charset=utf-8,%3Chtml%3E%3C%2Fhtml%3E",
),
true,
);
assert.equal(
isAllowedAnilistSetupNavigationUrl("https://example.com/redirect"),
false,
);
assert.equal(isAllowedAnilistSetupNavigationUrl("javascript:alert(1)"), false);
});

View File

@@ -0,0 +1,25 @@
const ANILIST_ALLOWED_HOSTS = new Set(["anilist.co", "www.anilist.co"]);
export function isAllowedAnilistExternalUrl(rawUrl: string): boolean {
try {
const parsedUrl = new URL(rawUrl);
return (
parsedUrl.protocol === "https:" &&
ANILIST_ALLOWED_HOSTS.has(parsedUrl.hostname.toLowerCase())
);
} catch {
return false;
}
}
export function isAllowedAnilistSetupNavigationUrl(rawUrl: string): boolean {
if (isAllowedAnilistExternalUrl(rawUrl)) {
return true;
}
try {
const parsedUrl = new URL(rawUrl);
return parsedUrl.protocol === "data:";
} catch {
return false;
}
}

View File

@@ -18,10 +18,27 @@ import type { RuntimeOptionsManager } from "../runtime-options";
import type { MecabTokenizer } from "../mecab-tokenizer";
import type { BaseWindowTracker } from "../window-trackers";
export interface AnilistSecretResolutionState {
status: "not_checked" | "resolved" | "error";
source: "none" | "literal" | "stored";
message: string | null;
resolvedAt: number | null;
errorAt: number | null;
}
export interface AnilistRetryQueueState {
pending: number;
ready: number;
deadLetter: number;
lastAttemptAt: number | null;
lastError: string | null;
}
export interface AppState {
yomitanExt: Extension | null;
yomitanSettingsWindow: BrowserWindow | null;
yomitanParserWindow: BrowserWindow | null;
anilistSetupWindow: BrowserWindow | null;
yomitanParserReadyPromise: Promise<void> | null;
yomitanParserInitPromise: Promise<boolean> | null;
mpvClient: MpvIpcClient | null;
@@ -33,6 +50,7 @@ export interface AppState {
currentMediaPath: string | null;
currentMediaTitle: string | null;
pendingSubtitlePosition: SubtitlePosition | null;
anilistClientSecretState: AnilistSecretResolutionState;
mecabTokenizer: MecabTokenizer | null;
keybindings: Keybinding[];
subtitleTimingTracker: SubtitleTimingTracker | null;
@@ -57,6 +75,8 @@ export interface AppState {
texthookerOnlyMode: boolean;
jlptLevelLookup: (term: string) => JlptLevel | null;
frequencyRankLookup: FrequencyDictionaryLookup;
anilistSetupPageOpened: boolean;
anilistRetryQueueState: AnilistRetryQueueState;
}
export interface AppStateInitialValues {
@@ -81,6 +101,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
yomitanExt: null,
yomitanSettingsWindow: null,
yomitanParserWindow: null,
anilistSetupWindow: null,
yomitanParserReadyPromise: null,
yomitanParserInitPromise: null,
mpvClient: null,
@@ -92,6 +113,13 @@ export function createAppState(values: AppStateInitialValues): AppState {
currentMediaPath: null,
currentMediaTitle: null,
pendingSubtitlePosition: null,
anilistClientSecretState: {
status: "not_checked",
source: "none",
message: null,
resolvedAt: null,
errorAt: null,
},
mecabTokenizer: null,
keybindings: [],
subtitleTimingTracker: null,
@@ -118,6 +146,14 @@ export function createAppState(values: AppStateInitialValues): AppState {
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
jlptLevelLookup: () => null,
frequencyRankLookup: () => null,
anilistSetupPageOpened: false,
anilistRetryQueueState: {
pending: 0,
ready: 0,
deadLetter: 0,
lastAttemptAt: null,
lastError: null,
},
};
}