calendit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +94 -0
- package/bin/cli.js +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +51 -0
- package/dist/commands/apply.d.ts +3 -0
- package/dist/commands/apply.js +67 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +36 -0
- package/dist/commands/cal.d.ts +3 -0
- package/dist/commands/cal.js +53 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +88 -0
- package/dist/commands/query.d.ts +3 -0
- package/dist/commands/query.js +64 -0
- package/dist/commands/shared.d.ts +16 -0
- package/dist/commands/shared.js +69 -0
- package/dist/core/applier.d.ts +26 -0
- package/dist/core/applier.js +141 -0
- package/dist/core/auth.d.ts +22 -0
- package/dist/core/auth.js +153 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +75 -0
- package/dist/core/datetime.d.ts +12 -0
- package/dist/core/datetime.js +61 -0
- package/dist/core/errors.d.ts +21 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/formatter.d.ts +27 -0
- package/dist/core/formatter.js +164 -0
- package/dist/core/logger.d.ts +9 -0
- package/dist/core/logger.js +68 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +95 -0
- package/dist/services/base.d.ts +52 -0
- package/dist/services/base.js +24 -0
- package/dist/services/google.d.ts +16 -0
- package/dist/services/google.js +151 -0
- package/dist/services/mock.d.ts +18 -0
- package/dist/services/mock.js +93 -0
- package/dist/services/outlook.d.ts +20 -0
- package/dist/services/outlook.js +163 -0
- package/dist/test_runner.d.ts +1 -0
- package/dist/test_runner.js +195 -0
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { parse as parseCsv } from "csv-parse/sync";
|
|
2
|
+
import { stringify as stringifyCsv } from "csv-stringify/sync";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ValidationError } from "./errors.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
6
|
+
const partialEventSchema = z.object({
|
|
7
|
+
id: z.string().optional(),
|
|
8
|
+
summary: z.string().min(1, "summary is required"),
|
|
9
|
+
start: z.string().datetime({ local: true }),
|
|
10
|
+
end: z.string().datetime({ local: true }),
|
|
11
|
+
location: z.string().optional(),
|
|
12
|
+
description: z.string().optional(),
|
|
13
|
+
});
|
|
14
|
+
export class Formatter {
|
|
15
|
+
/**
|
|
16
|
+
* 予定の配列を CSV 文字列に変換
|
|
17
|
+
*/
|
|
18
|
+
static toCsv(events) {
|
|
19
|
+
return stringifyCsv(events, {
|
|
20
|
+
header: true,
|
|
21
|
+
columns: ["id", "summary", "start", "end", "location", "description", "service", "calendarId"],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* CSV 文字列を予定の配列に変換
|
|
26
|
+
*/
|
|
27
|
+
static fromCsv(csv) {
|
|
28
|
+
const records = parseCsv(csv, {
|
|
29
|
+
columns: true,
|
|
30
|
+
skip_empty_lines: true,
|
|
31
|
+
});
|
|
32
|
+
return records;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 予定の配列を Markdown 文字列に変換
|
|
36
|
+
*/
|
|
37
|
+
static toMarkdown(events) {
|
|
38
|
+
let md = "# 予定一覧\n\n";
|
|
39
|
+
const grouped = this.groupByDate(events);
|
|
40
|
+
for (const [date, dayEvents] of Object.entries(grouped)) {
|
|
41
|
+
md += `## ${date}\n`;
|
|
42
|
+
for (const e of dayEvents) {
|
|
43
|
+
const timeRange = `${this.formatTime(e.start)} - ${this.formatTime(e.end)}`;
|
|
44
|
+
md += `- [ ] **${e.summary}** (${timeRange}) (ID: ${e.id || "new"})\n`;
|
|
45
|
+
if (e.description)
|
|
46
|
+
md += ` - ${e.description.replace(/\n/g, "\n ")}\n`;
|
|
47
|
+
}
|
|
48
|
+
md += "\n";
|
|
49
|
+
}
|
|
50
|
+
return md;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Markdown 文字列から予定を抽出
|
|
54
|
+
* 形式: - [ ] **Title** (HH:mm - HH:mm) (ID: id)
|
|
55
|
+
* インデントされた行は説明文として扱う
|
|
56
|
+
*/
|
|
57
|
+
static fromMarkdown(md, strict = false) {
|
|
58
|
+
const events = [];
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const lines = md.split("\n");
|
|
61
|
+
let currentDate = "";
|
|
62
|
+
let currentEvent = null;
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
const dateMatch = line.match(/^##\s+(\d{4}-\d{2}-\d{2})/);
|
|
66
|
+
if (dateMatch) {
|
|
67
|
+
currentDate = dateMatch[1];
|
|
68
|
+
currentEvent = null;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// イベント行のパース: - [ ] **Summary** (HH:mm - HH:mm) (ID: id)
|
|
72
|
+
// より柔軟に、末尾の (ID: ...) を優先的に探す。ID内のスペースや末尾のスペースを許容。
|
|
73
|
+
const eventRowMatch = line.match(/^-\s+\[.\]\s+(.+)\((ID:\s*[^)]+)\)\s*$/);
|
|
74
|
+
if (eventRowMatch && currentDate) {
|
|
75
|
+
const fullContent = eventRowMatch[1].trim();
|
|
76
|
+
const idPart = eventRowMatch[2];
|
|
77
|
+
const idMatch = idPart.match(/ID:\s+(.+)/);
|
|
78
|
+
const id = idMatch ? idMatch[1] : undefined;
|
|
79
|
+
// タイトルと時間帯を分離: **Summary** (HH:mm - HH:mm)
|
|
80
|
+
// 後ろから最初の (XX:XX - XX:XX) を探す
|
|
81
|
+
const timeMatch = fullContent.match(/\((?:\d{1,2}:\d{2})\s*-\s*(?:\d{1,2}:\d{2})\)$/);
|
|
82
|
+
if (timeMatch) {
|
|
83
|
+
const timeRangeStr = timeMatch[0];
|
|
84
|
+
const summaryPart = fullContent.replace(timeRangeStr, "").trim();
|
|
85
|
+
const summary = summaryPart.replace(/^\*\*(.+)\*\*$/, "$1"); // ** で囲まれていれば外す
|
|
86
|
+
const times = timeRangeStr.slice(1, -1).split("-").map(t => t.trim());
|
|
87
|
+
const start = `${currentDate}T${times[0]}:00`;
|
|
88
|
+
let end = `${currentDate}T${times[1]}:00`;
|
|
89
|
+
if (times[1] < times[0]) {
|
|
90
|
+
// End time is earlier than start time -> assume next day
|
|
91
|
+
const d = new Date(currentDate);
|
|
92
|
+
d.setDate(d.getDate() + 1);
|
|
93
|
+
const nextDay = d.toISOString().split("T")[0];
|
|
94
|
+
end = `${nextDay}T${times[1]}:00`;
|
|
95
|
+
}
|
|
96
|
+
currentEvent = {
|
|
97
|
+
id: id === "new" ? undefined : id,
|
|
98
|
+
summary,
|
|
99
|
+
start,
|
|
100
|
+
end,
|
|
101
|
+
description: "",
|
|
102
|
+
};
|
|
103
|
+
events.push(currentEvent);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
warnings.push(`[Line ${i + 1}] Skip: Time range format is invalid.`);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
else if (line.trim().startsWith("- [ ]")) {
|
|
111
|
+
warnings.push(`[Line ${i + 1}] Skip: Line matches event pattern but ID or Date is missing.`);
|
|
112
|
+
}
|
|
113
|
+
// 説明文のパース: インデントされた行 かつ 直前にイベントがある場合
|
|
114
|
+
if (currentEvent && line.startsWith(" ")) {
|
|
115
|
+
const descMatch = line.match(/^\s+-\s+(.+)$/);
|
|
116
|
+
const descLine = descMatch ? descMatch[1] : line.trim();
|
|
117
|
+
if (currentEvent.description) {
|
|
118
|
+
currentEvent.description += "\n" + descLine;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
currentEvent.description = descLine;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// それ以外の行はイベントの説明文モードを終了
|
|
126
|
+
if (line.trim() !== "" && !line.startsWith("- ")) {
|
|
127
|
+
// currentEvent = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [index, event] of events.entries()) {
|
|
132
|
+
const parsed = partialEventSchema.safeParse(event);
|
|
133
|
+
if (!parsed.success) {
|
|
134
|
+
const detail = parsed.error.issues.map((issue) => issue.message).join(", ");
|
|
135
|
+
const message = `Event #${index + 1} validation failed: ${detail}`;
|
|
136
|
+
if (strict) {
|
|
137
|
+
throw new ValidationError(message, "Markdown の予定行と時刻形式を確認してください。");
|
|
138
|
+
}
|
|
139
|
+
warnings.push(message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
warnings.forEach((warning) => logger.warn(warning));
|
|
143
|
+
return { events, warnings };
|
|
144
|
+
}
|
|
145
|
+
static groupByDate(events) {
|
|
146
|
+
const groups = {};
|
|
147
|
+
for (const e of events) {
|
|
148
|
+
const date = e.start.split("T")[0];
|
|
149
|
+
if (!groups[date])
|
|
150
|
+
groups[date] = [];
|
|
151
|
+
groups[date].push(e);
|
|
152
|
+
}
|
|
153
|
+
return groups;
|
|
154
|
+
}
|
|
155
|
+
static formatTime(iso) {
|
|
156
|
+
if (!iso)
|
|
157
|
+
return "??:??";
|
|
158
|
+
const date = new Date(iso);
|
|
159
|
+
// システムのタイムゾーンで HH:mm 形式にする
|
|
160
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
161
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
162
|
+
return `${hours}:${minutes}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
3
|
+
export declare function setDebugDump(filePath: string): void;
|
|
4
|
+
export declare const logger: {
|
|
5
|
+
debug: (...args: unknown[]) => void;
|
|
6
|
+
info: (...args: unknown[]) => void;
|
|
7
|
+
warn: (...args: unknown[]) => void;
|
|
8
|
+
error: (...args: unknown[]) => void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
const LEVEL_PRIORITY = {
|
|
3
|
+
debug: 10,
|
|
4
|
+
info: 20,
|
|
5
|
+
warn: 30,
|
|
6
|
+
error: 40,
|
|
7
|
+
};
|
|
8
|
+
let currentLevel = process.env.DEBUG === "calendit" ? "debug" : "info";
|
|
9
|
+
let debugStream = null;
|
|
10
|
+
export function setLogLevel(level) {
|
|
11
|
+
currentLevel = level;
|
|
12
|
+
}
|
|
13
|
+
export function setDebugDump(filePath) {
|
|
14
|
+
if (debugStream) {
|
|
15
|
+
debugStream.end();
|
|
16
|
+
debugStream = null;
|
|
17
|
+
}
|
|
18
|
+
debugStream = fs.createWriteStream(filePath, { flags: "a" });
|
|
19
|
+
}
|
|
20
|
+
function shouldLog(level) {
|
|
21
|
+
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
|
|
22
|
+
}
|
|
23
|
+
function writeToDebugDump(line) {
|
|
24
|
+
if (!debugStream)
|
|
25
|
+
return;
|
|
26
|
+
debugStream.write(`${line}\n`);
|
|
27
|
+
}
|
|
28
|
+
function stringifyArgs(args) {
|
|
29
|
+
return args
|
|
30
|
+
.map((arg) => {
|
|
31
|
+
if (typeof arg === "string")
|
|
32
|
+
return arg;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(arg);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return String(arg);
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
function emit(level, ...allArgs) {
|
|
43
|
+
const timestamp = new Date().toISOString().slice(11, 23);
|
|
44
|
+
const moduleOrMessage = allArgs[0];
|
|
45
|
+
const args = allArgs.slice(1);
|
|
46
|
+
const moduleTag = typeof moduleOrMessage === "string" && args.length > 0 ? `[${moduleOrMessage}]` : "";
|
|
47
|
+
const payload = moduleTag ? args : [moduleOrMessage, ...args];
|
|
48
|
+
const prefix = `[${level.toUpperCase()} ${timestamp}]${moduleTag}`;
|
|
49
|
+
const lineForDump = `${prefix} ${stringifyArgs(payload)}`.trim();
|
|
50
|
+
writeToDebugDump(lineForDump);
|
|
51
|
+
if (!shouldLog(level))
|
|
52
|
+
return;
|
|
53
|
+
if (level === "error") {
|
|
54
|
+
console.error(prefix, ...payload);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (level === "warn") {
|
|
58
|
+
console.warn(prefix, ...payload);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(prefix, ...payload);
|
|
62
|
+
}
|
|
63
|
+
export const logger = {
|
|
64
|
+
debug: (...args) => emit("debug", ...args),
|
|
65
|
+
info: (...args) => emit("info", ...args),
|
|
66
|
+
warn: (...args) => emit("warn", ...args),
|
|
67
|
+
error: (...args) => emit("error", ...args),
|
|
68
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { AuthManager } from "./core/auth.js";
|
|
3
|
+
import { ConfigManager } from "./core/config.js";
|
|
4
|
+
import { ApiError, AuthError, CalendarError, ConfigError, ValidationError } from "./core/errors.js";
|
|
5
|
+
import { logger, setDebugDump, setLogLevel } from "./core/logger.js";
|
|
6
|
+
import { registerAuthCommands } from "./commands/auth.js";
|
|
7
|
+
import { registerConfigCommands } from "./commands/config.js";
|
|
8
|
+
import { registerQueryCommand } from "./commands/query.js";
|
|
9
|
+
import { registerApplyCommand } from "./commands/apply.js";
|
|
10
|
+
import { registerAddCommand } from "./commands/add.js";
|
|
11
|
+
import { registerCalCommands } from "./commands/cal.js";
|
|
12
|
+
const program = new Command();
|
|
13
|
+
const deps = {
|
|
14
|
+
config: new ConfigManager(),
|
|
15
|
+
auth: new AuthManager(),
|
|
16
|
+
};
|
|
17
|
+
program
|
|
18
|
+
.name("calendit")
|
|
19
|
+
.description("Terminal-based Calendar Management Tool")
|
|
20
|
+
.version("2026-0416-01.02")
|
|
21
|
+
.option("--verbose", "Enable verbose debug logs", false)
|
|
22
|
+
.option("--debug-dump <file>", "Write all logs to a file");
|
|
23
|
+
program.hook("preAction", (thisCommand) => {
|
|
24
|
+
const opts = thisCommand.optsWithGlobals();
|
|
25
|
+
if (opts.debugDump) {
|
|
26
|
+
setDebugDump(opts.debugDump);
|
|
27
|
+
setLogLevel("debug");
|
|
28
|
+
logger.debug("Logger", `Debug dump enabled: ${opts.debugDump}`);
|
|
29
|
+
}
|
|
30
|
+
if (opts.verbose || process.env.DEBUG === "calendit" || opts.debugDump) {
|
|
31
|
+
setLogLevel("debug");
|
|
32
|
+
logger.debug("Logger", "Verbose mode enabled.");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
registerAuthCommands(program, deps);
|
|
36
|
+
registerConfigCommands(program, deps);
|
|
37
|
+
registerQueryCommand(program, deps);
|
|
38
|
+
registerApplyCommand(program, deps);
|
|
39
|
+
registerAddCommand(program, deps);
|
|
40
|
+
registerCalCommands(program, deps);
|
|
41
|
+
function handleError(error) {
|
|
42
|
+
const isVerbose = process.argv.includes("--verbose") || process.env.DEBUG === "calendit";
|
|
43
|
+
if (error instanceof ValidationError) {
|
|
44
|
+
logger.error(`入力エラー: ${error.message}`);
|
|
45
|
+
if (error.hint)
|
|
46
|
+
logger.info(`ヒント: ${error.hint}`);
|
|
47
|
+
if (isVerbose && error.stack)
|
|
48
|
+
logger.debug("STACK", error.stack);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (error instanceof ConfigError) {
|
|
52
|
+
logger.error(`設定エラー: ${error.message}`);
|
|
53
|
+
if (error.hint)
|
|
54
|
+
logger.info(`ヒント: ${error.hint}`);
|
|
55
|
+
if (isVerbose && error.stack)
|
|
56
|
+
logger.debug("STACK", error.stack);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof AuthError) {
|
|
60
|
+
logger.error(`認証エラー: ${error.message}`);
|
|
61
|
+
if (error.hint)
|
|
62
|
+
logger.info(`ヒント: ${error.hint}`);
|
|
63
|
+
if (isVerbose && error.stack)
|
|
64
|
+
logger.debug("STACK", error.stack);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof ApiError) {
|
|
68
|
+
const statusPart = error.statusCode ? ` (${error.statusCode})` : "";
|
|
69
|
+
logger.error(`API エラー${statusPart}: ${error.message}`);
|
|
70
|
+
if (error.hint)
|
|
71
|
+
logger.info(`ヒント: ${error.hint}`);
|
|
72
|
+
logger.debug("ApiError", "API error details", error.details);
|
|
73
|
+
if (isVerbose && error.stack)
|
|
74
|
+
logger.debug("STACK", error.stack);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof CalendarError) {
|
|
78
|
+
logger.error(error.message);
|
|
79
|
+
if (error.hint)
|
|
80
|
+
logger.info(`ヒント: ${error.hint}`);
|
|
81
|
+
if (isVerbose && error.stack)
|
|
82
|
+
logger.debug("STACK", error.stack);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const unknown = error;
|
|
86
|
+
logger.error(`予期しないエラー: ${unknown?.message || String(error)}`);
|
|
87
|
+
if (isVerbose && error instanceof Error && error.stack) {
|
|
88
|
+
logger.debug("STACK", error.stack);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
logger.debug("UnknownError", error);
|
|
92
|
+
}
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
program.parseAsync(process.argv).catch(handleError);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CalendarEvent, CalendarInfo, ProviderCapabilities } from "../types/index.js";
|
|
2
|
+
export interface ICalendarService {
|
|
3
|
+
getProviderId(): string;
|
|
4
|
+
getCapabilities(): ProviderCapabilities;
|
|
5
|
+
/**
|
|
6
|
+
* カレンダーの一覧を取得
|
|
7
|
+
*/
|
|
8
|
+
listCalendars(): Promise<CalendarInfo[]>;
|
|
9
|
+
/**
|
|
10
|
+
* カレンダーを作成
|
|
11
|
+
*/
|
|
12
|
+
createCalendar(name: string): Promise<CalendarInfo>;
|
|
13
|
+
/**
|
|
14
|
+
* カレンダーを削除
|
|
15
|
+
*/
|
|
16
|
+
deleteCalendar(calendarId: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* 指定期間の予定を取得
|
|
19
|
+
*/
|
|
20
|
+
listEvents(calendarId: string, start: Date, end: Date): Promise<CalendarEvent[]>;
|
|
21
|
+
/**
|
|
22
|
+
* 予定を追加
|
|
23
|
+
*/
|
|
24
|
+
createEvent(calendarId: string, event: Omit<CalendarEvent, "id" | "service" | "calendarId">): Promise<CalendarEvent>;
|
|
25
|
+
/**
|
|
26
|
+
* 予定を更新
|
|
27
|
+
*/
|
|
28
|
+
updateEvent(calendarId: string, eventId: string, event: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
29
|
+
/**
|
|
30
|
+
* 予定を削除
|
|
31
|
+
*/
|
|
32
|
+
deleteEvent(calendarId: string, eventId: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export declare abstract class AbstractCalendarService implements ICalendarService {
|
|
35
|
+
abstract getProviderId(): string;
|
|
36
|
+
abstract getCapabilities(): ProviderCapabilities;
|
|
37
|
+
abstract listCalendars(): Promise<CalendarInfo[]>;
|
|
38
|
+
abstract createCalendar(name: string): Promise<CalendarInfo>;
|
|
39
|
+
abstract deleteCalendar(calendarId: string): Promise<void>;
|
|
40
|
+
abstract listEvents(calendarId: string, start: Date, end: Date): Promise<CalendarEvent[]>;
|
|
41
|
+
abstract createEvent(calendarId: string, event: Omit<CalendarEvent, "id" | "service" | "calendarId">): Promise<CalendarEvent>;
|
|
42
|
+
abstract updateEvent(calendarId: string, eventId: string, event: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
43
|
+
abstract deleteEvent(calendarId: string, eventId: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* 予定データの正規化 (共通処理)
|
|
46
|
+
*/
|
|
47
|
+
protected normalizeEvent(event: Partial<CalendarEvent>): Partial<CalendarEvent>;
|
|
48
|
+
/**
|
|
49
|
+
* エラーの正規化 (各サービスでオーバーライド可能)
|
|
50
|
+
*/
|
|
51
|
+
protected normalizeError(error: any): Error;
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class AbstractCalendarService {
|
|
2
|
+
/**
|
|
3
|
+
* 予定データの正規化 (共通処理)
|
|
4
|
+
*/
|
|
5
|
+
normalizeEvent(event) {
|
|
6
|
+
const normalized = { ...event };
|
|
7
|
+
if (normalized.summary)
|
|
8
|
+
normalized.summary = normalized.summary.trim();
|
|
9
|
+
if (normalized.location)
|
|
10
|
+
normalized.location = normalized.location.trim();
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* エラーの正規化 (各サービスでオーバーライド可能)
|
|
15
|
+
*/
|
|
16
|
+
normalizeError(error) {
|
|
17
|
+
const providerId = this.getProviderId();
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
error.message = `[${providerId}] ${error.message}`;
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
return new Error(`[${providerId}] Unknown error: ${JSON.stringify(error)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AbstractCalendarService } from "./base.js";
|
|
2
|
+
import { CalendarEvent, CalendarInfo, ProviderCapabilities } from "../types/index.js";
|
|
3
|
+
export declare class GoogleCalendarService extends AbstractCalendarService {
|
|
4
|
+
private calendar;
|
|
5
|
+
constructor(auth: any);
|
|
6
|
+
private wrapApiError;
|
|
7
|
+
getProviderId(): string;
|
|
8
|
+
getCapabilities(): ProviderCapabilities;
|
|
9
|
+
listCalendars(): Promise<CalendarInfo[]>;
|
|
10
|
+
createCalendar(name: string): Promise<CalendarInfo>;
|
|
11
|
+
deleteCalendar(calendarId: string): Promise<void>;
|
|
12
|
+
listEvents(calendarId: string, start: Date, end: Date): Promise<CalendarEvent[]>;
|
|
13
|
+
createEvent(calendarId: string, event: Omit<CalendarEvent, "id" | "service" | "calendarId">): Promise<CalendarEvent>;
|
|
14
|
+
updateEvent(calendarId: string, eventId: string, event: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
15
|
+
deleteEvent(calendarId: string, eventId: string): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { google } from "googleapis";
|
|
2
|
+
import { AbstractCalendarService } from "./base.js";
|
|
3
|
+
import { ApiError } from "../core/errors.js";
|
|
4
|
+
import { logger } from "../core/logger.js";
|
|
5
|
+
export class GoogleCalendarService extends AbstractCalendarService {
|
|
6
|
+
calendar;
|
|
7
|
+
constructor(auth) {
|
|
8
|
+
super();
|
|
9
|
+
this.calendar = google.calendar({ version: "v3", auth });
|
|
10
|
+
}
|
|
11
|
+
wrapApiError(error, operation) {
|
|
12
|
+
const message = error instanceof Error ? error.message : "Unknown Google API error";
|
|
13
|
+
throw new ApiError(`Google API Error during ${operation}: ${message}`, {
|
|
14
|
+
provider: "google",
|
|
15
|
+
details: error,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
getProviderId() {
|
|
19
|
+
return "google";
|
|
20
|
+
}
|
|
21
|
+
getCapabilities() {
|
|
22
|
+
return {
|
|
23
|
+
webConferencing: true, // Google Meet support
|
|
24
|
+
bulkOperations: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async listCalendars() {
|
|
28
|
+
try {
|
|
29
|
+
const res = await this.calendar.calendarList.list();
|
|
30
|
+
return (res.data.items || []).map((item) => ({
|
|
31
|
+
id: item.id,
|
|
32
|
+
name: item.summary,
|
|
33
|
+
service: "google",
|
|
34
|
+
isPrimary: item.primary || false,
|
|
35
|
+
canEdit: item.accessRole === "owner" || item.accessRole === "writer",
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.wrapApiError(error, "listCalendars");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async createCalendar(name) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await this.calendar.calendars.insert({
|
|
45
|
+
requestBody: { summary: name },
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
id: res.data.id,
|
|
49
|
+
name: res.data.summary,
|
|
50
|
+
service: "google",
|
|
51
|
+
isPrimary: false,
|
|
52
|
+
canEdit: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.wrapApiError(error, "createCalendar");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async deleteCalendar(calendarId) {
|
|
60
|
+
try {
|
|
61
|
+
await this.calendar.calendars.delete({ calendarId });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.wrapApiError(error, "deleteCalendar");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async listEvents(calendarId, start, end) {
|
|
68
|
+
try {
|
|
69
|
+
logger.debug("Google listEvents", { calendarId, start: start.toISOString(), end: end.toISOString() });
|
|
70
|
+
const res = await this.calendar.events.list({
|
|
71
|
+
calendarId,
|
|
72
|
+
timeMin: start.toISOString(),
|
|
73
|
+
timeMax: end.toISOString(),
|
|
74
|
+
singleEvents: true,
|
|
75
|
+
orderBy: "startTime",
|
|
76
|
+
});
|
|
77
|
+
return (res.data.items || []).map((item) => ({
|
|
78
|
+
id: item.id,
|
|
79
|
+
summary: item.summary || "(No Title)",
|
|
80
|
+
start: item.start?.dateTime || item.start?.date,
|
|
81
|
+
end: item.end?.dateTime || item.end?.date,
|
|
82
|
+
location: item.location || undefined,
|
|
83
|
+
description: item.description || undefined,
|
|
84
|
+
service: "google",
|
|
85
|
+
calendarId,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.wrapApiError(error, "listEvents");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async createEvent(calendarId, event) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await this.calendar.events.insert({
|
|
95
|
+
calendarId,
|
|
96
|
+
requestBody: {
|
|
97
|
+
summary: event.summary,
|
|
98
|
+
start: { dateTime: event.start },
|
|
99
|
+
end: { dateTime: event.end },
|
|
100
|
+
location: event.location,
|
|
101
|
+
description: event.description,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
id: res.data.id,
|
|
106
|
+
summary: res.data.summary,
|
|
107
|
+
start: res.data.start?.dateTime,
|
|
108
|
+
end: res.data.end?.dateTime,
|
|
109
|
+
service: "google",
|
|
110
|
+
calendarId,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.wrapApiError(error, "createEvent");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async updateEvent(calendarId, eventId, event) {
|
|
118
|
+
try {
|
|
119
|
+
const res = await this.calendar.events.patch({
|
|
120
|
+
calendarId,
|
|
121
|
+
eventId,
|
|
122
|
+
requestBody: {
|
|
123
|
+
summary: event.summary,
|
|
124
|
+
start: event.start ? { dateTime: event.start } : undefined,
|
|
125
|
+
end: event.end ? { dateTime: event.end } : undefined,
|
|
126
|
+
location: event.location,
|
|
127
|
+
description: event.description,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
id: res.data.id,
|
|
132
|
+
summary: res.data.summary,
|
|
133
|
+
start: res.data.start?.dateTime,
|
|
134
|
+
end: res.data.end?.dateTime,
|
|
135
|
+
service: "google",
|
|
136
|
+
calendarId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.wrapApiError(error, "updateEvent");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async deleteEvent(calendarId, eventId) {
|
|
144
|
+
try {
|
|
145
|
+
await this.calendar.events.delete({ calendarId, eventId });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.wrapApiError(error, "deleteEvent");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AbstractCalendarService } from "./base.js";
|
|
2
|
+
import { CalendarEvent, CalendarInfo, ProviderCapabilities } from "../types/index.js";
|
|
3
|
+
export declare class MockCalendarService extends AbstractCalendarService {
|
|
4
|
+
private events;
|
|
5
|
+
private providerId;
|
|
6
|
+
constructor(providerId?: string);
|
|
7
|
+
private load;
|
|
8
|
+
private save;
|
|
9
|
+
getProviderId(): string;
|
|
10
|
+
getCapabilities(): ProviderCapabilities;
|
|
11
|
+
listCalendars(): Promise<CalendarInfo[]>;
|
|
12
|
+
createCalendar(name: string): Promise<CalendarInfo>;
|
|
13
|
+
deleteCalendar(calendarId: string): Promise<void>;
|
|
14
|
+
listEvents(calendarId: string, start: Date, end: Date): Promise<CalendarEvent[]>;
|
|
15
|
+
createEvent(calendarId: string, event: Omit<CalendarEvent, "id" | "service" | "calendarId">): Promise<CalendarEvent>;
|
|
16
|
+
updateEvent(calendarId: string, eventId: string, event: Partial<CalendarEvent>): Promise<CalendarEvent>;
|
|
17
|
+
deleteEvent(calendarId: string, eventId: string): Promise<void>;
|
|
18
|
+
}
|