LINE Business から「友だち」の LINE ID 一覧を作成する
GitHub リポジトリ
MIT ライセンスでご利用いただけます。
整理したバージョンを作りました。
記事を作成しました。
このスクラップについて
訳あって LINE Business から「友だち」の名前と LINE ID の一覧表を作成する必要が生じた。
このスクラップでは一覧表を作成するまでの過程を記録していく。
簡単な方法
連絡先ページにアクセスしてすべての「友だち」を表示させてから開発者ツールのコンソールで下記の式を実行する。
[...document.querySelectorAll('#content table tbody tr')].map((el) => [el.querySelector('td:nth-child(1)').innerText, el.querySelector('td:nth-child(4) a').href])
こうすることで下記のような JSON がコンソールに出力される。
[
[
"ここに名前が入ります",
"https://chat.line.biz/U11111111111111111111111111111111/chat/U22222222222222222222222222222222"
]
]
ただアカウントにチャットメッセージを送信したことがあるユーザーに限られるようだ。
後からわかったことですが U22222222222222222222222222222222 は LINE ユーザー ID とは異なるので注意が必要でした。
chat から推察されるようにチャット ID と呼ばれるようです。
お金を払う
GAS ラボさんという方が note で LINE 友だち一覧を出力するツールを 980 円で提供している。
プログラミングをしたくない方はこちらを買った方が早いし楽かもしれない。
使用する API
ユーザーリスト取得は認証済アカウントまたはプレミアムアカウントのみで利用できる。
GET https://api.line.me/v2/bot/followers/ids
GET https://api.line.me/v2/bot/followers/ids
GET https://api.line.me/v2/bot/profile/{userId}
この 2 つを組み合わせればいけそう。
下記ページが参考になった、ありがとうございます。
チャンネルアクセストークンの取得
下記のスクラップにある程度情報をまとめているので手順については割愛する。
やったこと
プロバイダー作成とチャンネルアクセストークン(長期)発行の 2 つだけ。
プロバイダー作成は LINE for Business 管理画面からもできるので便利。
プロジェクト作成
mkdir line-friend-export
cd line-friend-export
npm init -y
npm install dotenv node-fetch@2
npm install --save-dev ts-node @types/node @types/node-fetch@2
touch get-user-ids.ts get-profiles.ts .env
node-fetch のバージョン 2 指定の理由については下記が参考になれば幸いです。
コーディング
import fetch from "node-fetch";
type ResponseBody = {
userIds: string[];
next?: string;
};
async function main() {
const endpoint = "https://api.line.me/v2/bot/followers/ids";
let responseBody: ResponseBody | null = null;
for (;;) {
const searchParams = new URLSearchParams({
limit: "1000",
});
if (responseBody?.next) {
searchParams.set("start", responseBody.next);
}
const response = await fetch(endpoint + "?" + searchParams.toString(), {
headers: {
Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
},
});
if (response.status !== 200) {
console.error(await response.text());
return;
}
responseBody = (await response.json()) as ResponseBody;
for (const userId of responseBody.userIds) {
console.log(userId);
}
if (!responseBody.next) {
break;
}
}
}
main().catch((err) => console.error(err));
LINE_CHANNEL_ACCESS_TOKEN="xxxx"
実行
npx ts-node -r dotenv/config get-user-ids.ts > user-ids.txt
U11111111111111111111111111111111
U22222222222222222222222222222222
U33333333333333333333333333333333
...
LINE API のレート制限
あるのかな?
すぐに見つかった、相変わらず LINE はドキュメントが素晴らしい。
一部を除き、多くは 2,000リクエスト/秒 と考えて良さそう。
ユーザー ID からプロフィール取得するのに待ちタイマーを設けなくても良さそうだ。
プロフィール取得
import { readFile } from "fs/promises";
import fetch from "node-fetch";
type ResponseBody = {
displayName: string;
userId: string;
language?: string;
pictureUrl?: string;
statusMessage?: string;
};
async function main() {
const lines = (await readFile("user-ids.txt", "utf-8"))
.split("\n")
.filter((line) => line !== "");
for (const line of lines) {
const endpoint = `https://api.line.me/v2/bot/profile/${line}`;
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
},
});
if (response.status !== 200) {
console.error(await response.text());
}
const { displayName, userId, language, pictureUrl, statusMessage } =
(await response.json()) as ResponseBody;
console.log(
[
displayName,
userId,
language ?? "",
pictureUrl ?? "",
(statusMessage ?? "").replace(/\n/g, " "),
].join("\t")
);
}
}
main().catch((err) => console.error(err));
実行
npx ts-node -r dotenv/config get-profiles.ts > profiles.txt
ここに名前が入ります U11111111111111111111111111111111 ja https://profile.line-scdn.net/0m11111111111111111111111111111111111111111111
ここに名前が入ります U22222222222222222222222222222222 ja https://profile.line-scdn.net/0m22222222222222222222222222222222222222222222
ここに名前が入ります U33333333333333333333333333333333 ja https://profile.line-scdn.net/0m33333333333333333333333333333333333333333333
LINE ユーザー ID について
ユーザーIDは、同じユーザーであってもプロバイダーごとに異なる値が発行されます。プロバイダーが同じであれば、チャネルの種類(LINEログインチャネルやMessaging APIチャネル)にかかわらず、同じユーザーIDが割り当てられます。
これはかなり厄介だな。
LINE ユーザー ID についてさらに
つまりユーザー ID をエクスポートしてもプロバイダーが変わると利用できなくなる。
アカウント A でエクスポートしたユーザー ID をアカウント B で使うみたいなことは当然できない。
今回はチャネル A でエクスポートしたユーザー ID をチャネル B で使うのでセーフだった。
LINE チャットページから表示名を取得する
LINE チャットページではユーザーの表示名を自由に変更できる。
この表示名については取得するのがなかなか難しそうなので開発者ツールから無理やり取得してみる。
// タイムラインを表示する
document.querySelector('.list-group-item:nth-child(6) a div').click();
// エンピツボタンを押す
document.querySelector('#content-thirdly h3.d-inline-block a').click();
// 変更前の表示名の取得
document.querySelector('.modal-body .text-truncate-3').innerText
// 変更後の表示名の取得
document.querySelector('.modal-body input').value
プログラムにしてみる
(async () => {
try {
const divs = [
...document.querySelectorAll(".list-group-item a > div:first-child"),
].slice(0, 50);
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
const result = [];
for (const div of divs) {
// タイムラインを表示する
div.click();
await sleep(500);
// エンピツボタンを押す
document.querySelector("#content-thirdly h3.d-inline-block a").click();
await sleep(500);
// 変更前の表示名の取得
const before = document.querySelector(
".modal-body .text-truncate-3"
).innerText;
await sleep(500);
// 変更後の表示名の取得
const after = document.querySelector(".modal-body input").value;
await sleep(500);
// 閉じるボタンを押す
document.querySelector(".modal-header button").click();
result.push({ before, after });
}
console.log(result);
} catch (err) {
console.error(err);
}
})();
slice() の第 2 引数を指定すると 1〜2 件で試せる。
改良してみた
(async () => {
try {
const divs = [
...document.querySelectorAll(".list-group-item a > div:first-child"),
].slice(0, 10);
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
const findElement = async (selector, timeout, retry) => {
timeout = timeout ?? 100;
retry = retry ?? 5;
for (let i = 0; i < retry; i += 1) {
if (i > 0) {
await sleep(timeout);
}
const el = document.querySelector(selector);
if (el) {
return el;
}
}
throw new Error(`Element not found: selector = "${selector}"`);
};
const result = [];
for (const div of divs) {
// タイムラインを表示する
div.click();
// エンピツボタンを押す
(await findElement("#content-thirdly h3.d-inline-block a")).click();
// 変更前の表示名の取得
const before = (await findElement(".modal-body .text-truncate-3"))
.innerText;
// 変更後の表示名の取得
const after = (await findElement(".modal-body input")).value;
// 閉じるボタンを押す
(await findElement(".modal-header button")).click();
result.push({ before, after });
}
console.log(result);
} catch (err) {
console.error(err);
}
})();
上のコードは不要かも知れない
だいぶスピードアップしたが全部のユーザーを表示するのがかったるいなーと思って色々と調べているうちに下記にアクセスすれば必要なデータが JSON で取得できることがわかった。
GET https://chat.line.biz/api/v2/bots/U00000000000000000000000000000000/chats?folderType=ALL&limit=25
ドキュメントには無さそうなのでチャットアプリのための専用 API な感じがする。
limit は最大値が 25 のようだ。
レスポンスには list は next が含まれ、next が含まれる場合は次のリクエストのクエリパラメーターとして next を含めてやれば良さそう。
Cookie をコピーすれば Node.js からも取得できるかも知れないが、Chrome の開発者コンソールから実行した方が良さそう。
レスポンスを調べてみる
公式アカウント側が設定した表示名は内部的には nickname と呼ぶようだ。
また chatId というのがあるので Messaging API で取得できる userId とは違うもののようだ。
userId というのもあるがほとんどは chatId と同じになっているので chatId と同じと考えて良さそうだ。
チャット一覧を取得するプログラム
userId は事前に設定しておきます。
Google Chrome の開発者コンソールで実行します。
(async function () {
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
const endpoint = `https://chat.line.biz/api/v2/bots/${userId}/chats`;
const responseBodies = [];
let responseBody = null;
for (;;) {
const searchParams = new URLSearchParams({
folderType: "ALL",
limit: "25",
});
if (responseBody && responseBody.next) {
searchParams.set("next", responseBody.next);
}
const response = await fetch(endpoint + "?" + searchParams.toString());
if (response.status !== 200) {
console.error(await response.text());
return;
}
responseBody = await response.json();
responseBodies.push(responseBody);
if (!responseBody.next) {
break;
}
// 気休め
await sleep(200);
}
console.log(responseBodies);
})().catch((err) => console.error(err));
他にも困っている人がいるようなので後からコメントしておこう。
情報の抽出
レスポンスから下記の情報を抽出する。
- チャット ID
- 表示名
- ニックネーム
- アイコン URL
- 重複しているかどうか
import { readFile } from "fs/promises";
async function main() {
const text = await readFile("data-chats.json", "utf-8");
const items = JSON.parse(text) as {
list: {
chatId: string;
profile: {
name?: string;
nickname?: string;
iconHash?: string;
};
}[];
}[];
const users: {
chatId: string;
name: string;
nickname: string;
icon: string;
}[] = [];
for (const item of items) {
for (const listItem of item.list) {
if (!listItem.profile?.name) {
continue;
}
const chatId = listItem.chatId;
const name = listItem.profile.name;
const nickname = listItem.profile.nickname ?? "";
const icon = listItem.profile.iconHash
? `https://profile.line-scdn.net/${listItem.profile.iconHash}/preview`
: "";
const user = { chatId, name, nickname, icon };
users.push(user);
}
}
const userNames = users.map((user) => user.name);
const duplicateNames: string[] = [];
userNames.forEach((userName, i) => {
if (userNames.indexOf(userName) !== i) {
duplicateNames.push(userName);
}
});
const duplicateNamesSet = new Set(duplicateNames);
console.log(
JSON.stringify(
users.map((user) => ({
...user,
isDuplicated: duplicateNamesSet.has(user.name),
})),
null,
2
)
);
}
main().catch((err) => console.error(err));
npx ts-node extract-nicknames.ts > data-nicknames.json
プロフィールのデータ形式変更
今さらだけど TSV じゃなくて JSON のままにしておけばよかった。
という訳でソースコードを少し改良する。
import { readFile } from "fs/promises";
import fetch from "node-fetch";
type ResponseBody = {
displayName: string;
userId: string;
language?: string;
pictureUrl?: string;
statusMessage?: string;
};
async function main() {
const lines = (await readFile("data-user-ids.txt", "utf-8"))
.split("\n")
.filter((line) => line !== "");
const responseBodies = [] as any[];
for (const line of lines) {
const endpoint = `https://api.line.me/v2/bot/profile/${line}`;
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
},
});
if (response.status !== 200) {
console.error(await response.text());
continue;
}
responseBodies.push(await response.json());
}
console.log(JSON.stringify(responseBodies, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config get-profiles.ts > data-profiles.json
名寄せ?結合?
data-profiles.json と data-nicknames.json(data-chats.json の方が良かったかな?)を名寄せする。
名寄せと呼べば良いのか、結合と呼べば良いのか、定まらない。
import { readFile } from "fs/promises";
async function main() {
const chats: {
chatId: string;
name: string;
nickname: string;
icon: string;
isDuplicated: boolean;
}[] = JSON.parse(await readFile("data-nicknames.json", "utf-8"));
const profiles: {
userId: string;
displayName: string;
pictureUrl?: string;
}[] = JSON.parse(await readFile("data-profiles.json", "utf-8"));
const newProfiles = profiles.map((profile) => {
const { userId, displayName, pictureUrl } = profile;
const filteredChats = chats.filter((chat) => {
return chat.name === profile.displayName;
});
if (filteredChats.length === 0) {
return profile;
} else if (filteredChats.length === 1) {
return {
...profile,
nickname: filteredChats[0].nickname,
};
} else {
return {
...profile,
candidates: filteredChats.map((chat) => ({
chatId: chat.chatId,
nickname: chat.nickname,
icon: chat.icon,
})),
};
}
});
console.log(JSON.stringify(newProfiles, null, 2));
}
main().catch((err) => console.error(err));
npx ts-node append-nicknames.ts > data-append.json
名前が同じで判断がつかないものについては candidates
をつけた。
ここまできたら写真を見て手動で判断するしかない。
ただ 123 件も重複があるのでなかなかにかったるい。
画像が使えそうだ
Messaging API と Chat API を使って出力したそれぞれのデータにはアイコンの URL が含まれており、アイコン画像をダウンロードしてハッシュを計算し、一致する場合には同一人物して扱えば良いかも知れない。
Chat API の方の URL には "/preview" が末尾にあるが、これを削除するとオリジナルサイズの画像を取得できそう。
とりあえず現状
import { readFile } from "fs/promises";
async function main() {
const chats: {
chatId: string;
name: string;
nickname: string;
icon: string;
isDuplicated: boolean;
}[] = JSON.parse(await readFile("data-nicknames.json", "utf-8"));
const profiles: {
userId: string;
displayName: string;
pictureUrl?: string;
}[] = JSON.parse(await readFile("data-profiles.json", "utf-8"));
const mappings = (await readFile("mappings.txt", "utf-8"))
.split("\n")
.filter((line) => line !== "")
.map((line) => {
const [userId, chatId] = line.split(" ");
return { userId, chatId };
});
const newProfiles = profiles.map((profile) => {
const filteredChats = chats.filter((chat) => {
return chat.name === profile.displayName;
});
if (filteredChats.length === 0) {
return profile;
} else if (filteredChats.length === 1) {
return {
...profile,
nickname: filteredChats[0].nickname,
};
} else {
const filteredMappings = mappings.filter((mapping) => {
return mapping.userId === profile.userId;
});
if (filteredMappings.length === 0) {
return {
...profile,
candidates: filteredChats.map((chat) => ({
chatId: chat.chatId,
nickname: chat.nickname,
icon: chat.icon,
})),
};
} else if (filteredMappings.length === 1) {
const chat = filteredChats.find(
(chat) => chat.chatId === filteredMappings[0].chatId
);
if (!chat) {
throw new Error("!chat");
}
return {
...profile,
nickname: chat.nickname,
};
} else {
throw new Error("filteredMappings.length >= 2");
}
}
});
console.log(JSON.stringify(newProfiles, null, 2));
}
main().catch((err) => console.error(err));
画像を比較する
あらかじめ data-nicknames.json の icon から "/preview" を削除しておく。
import { createHash } from "crypto";
import { readFile } from "fs/promises";
import fetch from "node-fetch";
async function main() {
const chats: {
chatId: string;
name: string;
nickname: string;
icon: string;
isDuplicated: boolean;
}[] = JSON.parse(await readFile("data-nicknames.json", "utf-8"));
const profiles: {
userId: string;
displayName: string;
pictureUrl?: string;
}[] = JSON.parse(await readFile("data-profiles.json", "utf-8"));
const mappings = (await readFile("mappings.txt", "utf-8"))
.split("\n")
.filter((line) => line !== "")
.map((line) => {
const [userId, chatId] = line.split(" ");
return { userId, chatId };
});
const newProfiles = await Promise.all(
profiles.map(async (profile) => {
const filteredChats = chats.filter((chat) => {
return chat.name === profile.displayName;
});
if (filteredChats.length === 0) {
return profile;
} else if (filteredChats.length === 1) {
return {
...profile,
nickname: filteredChats[0].nickname,
chatId: filteredChats[0].chatId,
};
} else {
const candidates = filteredChats.map((chat) => ({
chatId: chat.chatId,
nickname: chat.nickname,
icon: chat.icon,
}));
if (profile.pictureUrl) {
const pictureDigest = await getImageHash(profile.pictureUrl);
for (const candidate of candidates) {
if (candidate.icon !== "") {
const iconDigest = await getImageHash(candidate.icon);
if (pictureDigest === iconDigest) {
return {
...profile,
nickname: filteredChats[0].nickname,
chatId: filteredChats[0].chatId,
};
}
}
}
}
return {
...profile,
candidates,
};
}
})
);
console.log(JSON.stringify(newProfiles, null, 2));
}
async function getImageHash(url: string): Promise<string> {
const arrayBuffer = await fetch(url).then((res) => res.arrayBuffer());
const buffer = Buffer.from(arrayBuffer);
return createHash("sha-256").update(buffer).digest("hex");
}
main().catch((err) => console.error(err));
これで 100 件あった重複が 10 件くらいに減った。
最後かな?
chatId の Set を作成し、candidates の要素数を少しでも減らそう。
import { readFile } from "fs/promises";
async function main() {
const users: {
chatId?: string;
candidates?: {
chatId: string;
}[];
}[] = JSON.parse(await readFile("data-append.json", "utf-8"));
const chatIds = users
.filter((user) => user.chatId)
.map((user) => user.chatId) as string[];
const chatIdsSet = new Set(chatIds);
const newUsers = users.map((user) => {
if (!user.candidates) {
return {
...user,
chatUrl: getChatUrl(user.chatId),
};
}
return {
...user,
candidates: user.candidates
.filter((candidate) => {
return chatIdsSet.has(candidate.chatId);
})
.map((candidate) => {
return {
...candidate,
chatUrl: getChatUrl(candidate.chatId),
};
}),
};
});
console.log(JSON.stringify(newUsers, null, 2));
}
function getChatUrl(chatId?: string): string {
return chatId
? `https://chat.line.biz/${process.env.LINE_CHANNEL_USER_ID}/chat/${chatId}`
: "";
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config remove-candidates.ts > data-last.json
ついでに chatUrl を付加した。
最初から実行する。
npx ts-node -r dotenv/config get-user-ids.ts > data-user-ids.txt
npx ts-node -r dotenv/config get-profiles.ts > data-profiles.json
# ブラウザの開発者コンソールで get-chats.js を実行して data-chats.json に保存する。
# その際に const userId = "Uxxxx"; を忘れないようにする
npx ts-node -r dotenv/config extract-nicknames.ts > data-nicknames.json
npx ts-node -r dotenv/config append-nicknames.ts > data-append.json
npx ts-node -r dotenv/config remove-candidates.ts > data-last.json
なんかおかしいと思ったら /preview を消すのを忘れていた。
再度実行してみたけどやはり 10 件ちょっとくらいは candidates が残っている。
ただ candidates も 1 件しか無いのでほとんど絞られていると言っても過言ではない。
形を整えてみる
タブ区切りにして Excel にコピペできるようにしてみよう。
import { readFile } from "fs/promises";
async function main() {
const users: any[] = JSON.parse(await readFile("data-last.json", "utf-8"));
const headerCells = [
"LINE ID",
"お名前",
"お客さまが設定したお名前",
"チャットページのURL",
// "【候補】お客さまが設定したお名前",
// "【候補】チャットページのURL",
];
console.log(headerCells.join("\t"));
for (const user of users) {
const { userId, displayName, nickname, chatUrl } = user;
const cells = [userId, nickname, displayName, chatUrl];
if (!nickname) {
continue;
}
if (user.candidates) {
for (const candidate of user.candidates) {
cells.push(candidate.nickname);
cells.push(candidate.chatUrl);
}
}
console.log(cells.join("\t"));
}
}
main().catch((err) => console.error(err));
npx ts-node -r dotenv/config export-tsv.ts > data-export.json
ややこしいのでニックネームがない場合は取り除くことにした。
おわりに
LINE ID とプロフィールのエクスポートは API が用意されていたので簡単だった。
一方、ニックネーム(チャットページでこちら側で設定した名前)については結構大変だった。
たまたま隠し API を見つけられたから良かったが、そうじゃなかったらかなり大変だっただろう。
かなりとっ散らかってしまったのでどうにか機会を作って整理して記事にしたい。
とりあえずこの「おわりに」を書いたら下記の記事にコメントしておこう。
コメントの写し
Mako さん、はじめまして。
プログラマーの薄田と申します。
残念ながら Messaging APIで取得できるデータの中に店舗側が設定した「ユーザーの表示名」は含まれないようです。
ただし方法が無い訳ではなく、まだまだ不完全ながら解決方法を見つけましたので共有いたします。
もう 1 カ月以上経っており別の方法で解決されたかも知れませんが、
他の方のお役に立てばと思い回答させていただきます。
解決の過程については下記の記事にまとめてました。
ポイント
下記の API を使用して店舗側で設定した「ユーザーの表示名」を取得することができます。
GET https://chat.line.biz/api/v2/bots/U00000000000000000000000000000000/chats?folderType=ALL&limit=25
U0000... のところにはチャットページに表示される U1234... みたいな文字列が入ります。
ドキュメントについては僕が少し探したところ見つからなかったので隠し API かも知れません。
したがって今と将来とでは仕様が変化する可能性があるのでご注意ください。
手順
下記の通りです。
- LINE 公式アカウントの管理画面にログインしてチャットページへ移動する。
- ブラウザの開発者ツールを使って下記の JavaScript コードを実行する。
(async function () {
const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
const endpoint = `https://chat.line.biz/api/v2/bots/${userId}/chats`;
const responseBodies = [];
let responseBody = null;
for (;;) {
const searchParams = new URLSearchParams({
folderType: "ALL",
limit: "25",
});
if (responseBody && responseBody.next) {
searchParams.set("next", responseBody.next);
}
const response = await fetch(endpoint + "?" + searchParams.toString());
if (response.status !== 200) {
console.error(await response.text());
return;
}
responseBody = await response.json();
responseBodies.push(responseBody);
if (!responseBody.next) {
break;
}
// 気休め
await sleep(200);
}
console.log(responseBodies);
})().catch((err) => console.error(err));
しばらくするとコンソールに実行結果が出力されるのでコピーします。
実行結果の中に nickname
という名称で店舗側が設定した「ユーザーの表示名」が含まれています。
userId が異なる点について
実行結果には userId
も含まれているのですが、この userId
は Messaging API で取得できる API とは異なるようです。
したがって Messaging API の userId + 店舗側が設定した「ユーザーの表示名」の対応表が欲しい場合には名寄せ作業が必要になります。
この名寄せ作業が結構大変なのですが僕の場合は下記のようにユーザーの同一性を判断しました。
- 友だちが設定した表示名が同じであれば同じユーザーと判断する。
- アイコン画像が同じであれば同じユーザーと判断する。
Messaging API で出力されたユーザーは 1,350 件で、最終的に残ったのは 1,064 件でした。
GitHub リポジトリ
下記のプログラムが含まれた GitHub リポジトリを作成しましたのでお役に立つようであればご活用ください。
- Messaging API を使用したユーザー ID&プロフィールの出力
- 隠し API を使用した店舗側で設定した「ユーザーの表示名」の出力
- 名寄せ作業
append-nicknames.ts の修正
よく考えたら一致する名前が 1 つしか無いからと言ってそれが正しいとは限らない。
基本的にはすべてについて画像を比較する必要がある。
また、map + async を使うと並列で画像取得リクエストが何件も送信されるので時々失敗する。
これらの点を改良した。
import { createHash } from "crypto";
import { readFile } from "fs/promises";
import fetch from "node-fetch";
async function main() {
const chats: {
chatId: string;
name: string;
nickname: string;
icon: string;
isDuplicated: boolean;
}[] = JSON.parse(await readFile("data-nicknames.json", "utf-8"));
type Profile = {
userId: string;
displayName: string;
pictureUrl?: string;
};
type Candidate = {
chatId: string;
nickname: string;
icon: string;
};
const profiles: Profile[] = JSON.parse(
await readFile("data-profiles.json", "utf-8")
);
const newProfiles: (
| Profile
| { nickname?: string; chatId?: string; candidates?: Candidate[] }
)[] = [];
for (const profile of profiles) {
const chatsWithSameName = chats.filter((chat) => {
return chat.name === profile.displayName;
});
if (chatsWithSameName.length === 0) {
newProfiles.push(profile);
continue;
}
const candidates = chatsWithSameName.map((chat) => ({
chatId: chat.chatId,
nickname: chat.nickname,
icon: chat.icon,
}));
newProfiles.push(
await (async () => {
if (profile.pictureUrl) {
const pictureDigest = await getImageHash(profile.pictureUrl);
for (const candidate of candidates) {
if (candidate.icon !== "") {
const iconDigest = await getImageHash(candidate.icon);
if (pictureDigest === iconDigest) {
return {
...profile,
nickname: chatsWithSameName[0].nickname,
chatId: chatsWithSameName[0].chatId,
};
}
}
}
}
return {
...profile,
candidates,
};
})()
);
}
console.log(JSON.stringify(newProfiles, null, 2));
}
async function getImageHash(url: string): Promise<string> {
const arrayBuffer = await fetch(url).then((res) => res.arrayBuffer());
const buffer = Buffer.from(arrayBuffer);
return createHash("sha-256").update(buffer).digest("hex");
}
main().catch((err) => console.error(err));