aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/fetch-logoform-events.mjs
blob: 650e5307c0dbc37f09f8021f369c9b8e6de54f89 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// LoGoフォーム(小平市)の手続き一覧を取得する。
//
// LoGoフォームのページ自体は自動アクセス防止(TRUSTBANKの確認画面)で
// ブラウザから開けないことがある。一方、一覧のAPIは直接呼べる。
// ただし Node の fetch では 403 になるため、curl を使って取得している。
//
// 使い方:
//   node scripts/fetch-logoform-events.mjs                 # イベント情報(49006)
//   node scripts/fetch-logoform-events.mjs 49789            # 市議会からのお知らせ
//   node scripts/fetch-logoform-events.mjs 49006 --json
//   node scripts/fetch-logoform-events.mjs --uri 2524        # 手続きページの番号から自動で調べる
//
// 手続きページ(https://logoform.jp/procedure/exdH/<uri>)の内部IDは、
// ページを開いたときに呼ばれる一覧APIのURL(.../detail/search/<id>)に出る。
// --uri を指定すると、ブラウザで開いてそのURLからIDを調べる。
//
// 既知の内部ID:
//   49006 … exdH/2517 イベント情報
//   48668 … exdH/666  電子申請(手続き一覧)
//   49789 … exdH/2524 市議会からのお知らせ

import { execFileSync } from 'node:child_process';

const KNOWN_IDS = {
  '2517': '49006',
  '666': '48668',
  '2524': '49789',
};

const UA =
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';

const args = process.argv.slice(2);
const asJson = args.includes('--json');
const uriIndex = args.indexOf('--uri');
const uri = uriIndex >= 0 ? args[uriIndex + 1] : null;
const directId = args.find((a) => /^\d+$/.test(a));

let procedureId = directId ?? '49006';

if (uri) {
  procedureId = KNOWN_IDS[uri] ?? (await discoverId(uri));
}

/** 手続きページをブラウザで開き、一覧APIのURLから内部IDを調べる */
async function discoverId(procedureUri) {
  const { chromium } = await import('playwright');
  const browser = await chromium.launch({
    args: ['--disable-blink-features=AutomationControlled', '--no-sandbox'],
  });
  const context = await browser.newContext({
    locale: 'ja-JP',
    userAgent: UA,
    viewport: { width: 1400, height: 1000 },
  });
  await context.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
  });

  let found = null;
  const page = await context.newPage();
  page.on('response', (res) => {
    const match = res.url().match(/\/api\/procedure\/branch\/detail\/search\/(\d+)/);
    if (match) found = match[1];
  });

  await page.goto(`https://logoform.jp/procedure/exdH/${procedureUri}`, {
    waitUntil: 'domcontentloaded',
    timeout: 60000,
  });
  for (let i = 0; i < 10 && !found; i++) await page.waitForTimeout(2000);
  await browser.close();

  if (!found) throw new Error(`内部IDが分かりませんでした(exdH/${procedureUri})`);
  return found;
}

// 空のオブジェクトで全件返る。page などの項目を足すと「不明な項目」で弾かれる。
const curlArgs = [
  '-s',
  '--max-time',
  '30',
  '-X',
  'POST',
  '-H',
  'Content-Type: application/json',
  '-H',
  'Accept: application/json',
  '-H',
  `Referer: https://logoform.jp/procedure/exdH/${uri ?? '2517'}`,
  '-H',
  `User-Agent: ${UA}`,
  '-d',
  '{}',
  `https://logoform.jp/api/procedure/branch/detail/search/${procedureId}`,
];

let raw;
try {
  raw = execFileSync('curl', curlArgs, { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
} catch (error) {
  console.error('curl の実行に失敗しました。curl が使えるか確認してください。');
  console.error(String(error).slice(0, 300));
  process.exit(1);
}

let data;
try {
  data = JSON.parse(raw);
} catch {
  console.error('JSONとして読めませんでした(自動アクセス防止の画面が返っている可能性があります)。');
  console.error(raw.slice(0, 300));
  process.exit(1);
}

const items = Array.isArray(data.items) ? data.items : [];

if (asJson) {
  console.log(JSON.stringify(items, null, 2));
  process.exit(0);
}

console.log(`procedure_id=${procedureId} 件数=${items.length}`);
for (const item of items) {
  const description = (item.description ?? '').replace(/\s+/g, ' ').trim();
  console.log(
    `- [${item.category_name}] ${item.procedure_name}` +
      (description ? `\n    ${description.slice(0, 120)}` : '') +
      `\n    https://logoform.jp${item.form_url ?? ''}`,
  );
}