Skip to content

Commit

Permalink
Url preview (#45)
Browse files Browse the repository at this point in the history
* [add] - link-preview-js

* [add] - 履歴用DBの構造処理を更新

URLプレビュー用データ追加

* [add] - リンクプレビューデータ用のinterface追加

* [add] - linkDataを追加するmigrationスクリプト作成

* [change] - linkDataのカラムデータタイプを変更

* [add] - migraitonスクリプトを適用

* [change] - SQLiteにblobを適用したことによりinterfaceを更新

* [add] - 型補間

* [fix] - 重複したカラムへの対応忘れてた

* [change] - リンクプレビューデータの型をblobからテキストへ

JSONだけで保存することにした

* [change] - リンクプレビューデータ用のinterface更新

* [change] - linkDataの型更新を適用

* [change] - interfaceの一部統一

* [add] - メッセージ単体取得用のパース処理にリンクプレビューデータ分も追加

* [remove/add] - link-preview-jsからopen-graph-scaperへ

* [add] - WIP リンクプレビュー生成関数の土台作成

* [add] - リンクプレビューデータのinterface更新

* [add] - これも

* [add] - 履歴取得時、リンクプレビューがnullだった時空JSONにするように

* [add] - リンクのプレビュー処理"だけ"をしてみる

* [change] - コメント

* [add] - WIP プレビュー時データをマージして送信するように

* [add] - URLプレビュー用のOGデータを単一URLだけパース、記録するように

* [change] - プレビューデータが取れた時のみ更新するように

* [change] - 美化

* [add] - linkData用interfaceにfavicon

* [change] - メッセージへのURLプレビュー追加処理の返し方更新

* [fix] - !WIP:リンクプレビューデータのJSONマージができていなかった

* [add] - favionもパースするように

* [add] - メッセ用Interfaceのリンクデータにtitle追加

* [add] - リンクデータプレビュー処理時titleのパースを追加

* [add] - ウェブサイトに加えてmediaTypeがarticleのものもパースするように

* [change] - 画像用のリンクデータのinterface更新

?をつけただけ

* [change] - URL判別用regexを改善

* [change] - 画像用リンクならリンクプレビュー処理をしないように

* [change] - メディア系以外のURLなら無理やりパースするように

* [add] - FxTwitter用のパース方法を追加

* [fix] - 画像用のinterfaceが違う

* [change] - URLメタデータパース部分を関数化

* [change] - importの位置

* [change] - リンクプレビューデータ用のinterfaceを全体的に更新

* [change] - リンクデータのinterface更新に伴うパース処理途中の型定義

* [change] - URL配列のパース処理に対応

* [add] - 画像単体用のためのmediaTypeも追加

* [fix] - Twitterのリンクデータ取得に使うURLが最初の1個目しか使われていなかった

* [remove] - 不要なログ
  • Loading branch information
NfoAlex authored Jun 7, 2024
1 parent b217a00 commit 97c2a8c
Show file tree
Hide file tree
Showing 10 changed files with 546 additions and 34 deletions.
301 changes: 268 additions & 33 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"express": "^4.18.3",
"install": "^0.13.0",
"multer": "^1.4.5-lts.1",
"open-graph-scraper": "^6.5.2",
"socket.io": "^4.7.4",
"sqlite3": "^5.1.7"
},
Expand Down
9 changes: 9 additions & 0 deletions src/actionHandler/Message/fetchHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,17 @@ export default async function fetchHistory(
let historyParsed:IMessage[] = [];
//パース処理
for (let index in history) {
//リンクプレビューのJSONパース、nullなら空JSONに
const linkDataParsed:IMessage["linkData"] =
history[index].linkData!==null
?
JSON.parse(history[index].linkData)
:
{};

historyParsed.push({
...history[index],
linkData: linkDataParsed,
reaction: JSON.parse(history[index].reaction)
});
}
Expand Down
1 change: 1 addition & 0 deletions src/actionHandler/Message/fetchMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function fetchMessage(
//生メッセージデータを扱える形にパースする
const messageParsed:IMessage = {
...message[0],
linkData: JSON.parse(message[0].linkData),
reaction: JSON.parse(message[0].reaction)
};
resolve(messageParsed);
Expand Down
1 change: 1 addition & 0 deletions src/actionHandler/Message/saveMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default async function saveMessage(
channelId: message.channelId,
userId: userId,
content: message.content,
linkData: {},
time: "",
reaction: {}
};
Expand Down
6 changes: 6 additions & 0 deletions src/db/InitMessage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import sqlite3 from "sqlite3";
const db = new sqlite3.Database("./records/MESSAGE.db");

import migration20240603 from "./migration/Message/20240603";

db.serialize(() => {
//migration
migration20240603();

//randomチャンネル用のテーブル作成
db.run(
`create table if not exists C0001(
messageId TEXT PRIMARY KEY,
channelId TEXT NOT NULL,
userId TEXT NOT NULL,
content TEXT NOT NULL,
linkData TEXT DEFAULT '{}',
time TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
reaction TEXT NOT NULL
)`);
Expand Down
23 changes: 23 additions & 0 deletions src/db/migration/Message/20240603.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sqlite3 from "sqlite3";
const db = new sqlite3.Database("./records/MESSAGE.db");

/**
* すべてのチャンネル履歴テーブルへlinkDataカラムを追加
*/
export default async function migration20240603() {
//チャンネル分のテーブルへlinkDataカラムを追加
db.all(
`
SELECT name FROM sqlite_master WHERE type='table';
`,
(err:Error, tables:[{name:string}]) => {
//console.log("20240603 :: tables->", tables);

//ループしてlinkDataカラムを追加
for (let channelName of tables) {
db.run(`ALTER TABLE ` + channelName.name + ` ADD linkData TEXT DEFAULT '{}'`, (err:Error)=>{});
}
return;
}
);
}
32 changes: 31 additions & 1 deletion src/socketHandler/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import fetchMessage from "../actionHandler/Message/fetchMessage";
import setMessageReadTime from "../actionHandler/Message/setMessageReadId";
import getMessageReadId from "../actionHandler/Message/getMessageReadId";
import deleteMessage from "../actionHandler/Message/deleteMessage";
import genLinkPreview from "../util/genLinkPreview";

import IRequestSender from "../type/requestSender";
import type IRequestSender from "../type/requestSender";
import type { IMessage, IMessageReadId } from "../type/Message";

module.exports = (io:Server) => {
Expand Down Expand Up @@ -47,6 +48,35 @@ module.exports = (io:Server) => {
//処理に成功したのならメッセージ送信
if (messageData !== null) {
io.to(messageData.channelId).emit("receiveMessage", messageData);

//URLが含まれるならプレビューを生成
const urlRegex = /((https|http)?:\/\/[^\s]+)/g;
const urlMatched = messageData.content.match(urlRegex);
//nullじゃなければ生成
if (urlMatched) {
const linkDataResult:IMessage["linkData"]|null = await genLinkPreview(
urlMatched,
messageData.channelId,
messageData.messageId
);

//結果があるなら更新させる
if (linkDataResult !== null) {
//リンクデータを上書き
messageData.linkData = linkDataResult;
//送信
io.to(messageData.channelId).emit(
"updateMessage",
{
result: "SUCCESS",
data: messageData
}
);
} else {
console.log("Message :: socket(sendMessage) : URL結果がnull");
return;
}
}
}
} catch(e) {
console.log("Message :: socket(sendMessage) : エラー->", e);
Expand Down
20 changes: 20 additions & 0 deletions src/type/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface IMessageBeforeParsing {
channelId: string,
userId: string,
content: string,
linkData: string,
time: string,
reaction: string
}
Expand All @@ -14,6 +15,25 @@ export interface IMessage {
channelId: string,
userId: string,
content: string,
linkData: {
[key: string]:
{
contentType: "text/html"|"video",
mediaType: string,
url: string,
siteName?: string,
title?: string,
description?: string,
favicon: string,
images?: {url:string, type:string}[]
}
|
{
contentType: "image",
mediaType: "image",
url: string
}
},
time: string,
reaction: {
[key: string]: {
Expand Down
186 changes: 186 additions & 0 deletions src/util/genLinkPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import sqlite3 from "sqlite3";
import { IMessage } from "../type/Message";
const db = new sqlite3.Database("./records/MESSAGE.db");
import ogs from 'open-graph-scraper';

//取得できるOpenGraphデータのinterface
interface IOGData {
ogSiteName: string,
ogTitle: string,
ogType: string,
ogUrl: string,
ogImage: {url:string, type:string}[],
ogDescription: string,
favicon: string,
requestUrl: string,
success: boolean
};

//取得したメタデータをパースしたもの
interface IURLParsed {
contentType: "text/html",
mediaType: string,
url: string,
siteName: string,
title: string,
description: string,
images: {url:string, type:string}[],
favicon: string
};

export default async function genLinkPreview(
urls:RegExpMatchArray,
channelId:string,
messageId: string
):Promise<IMessage["linkData"]|null> {
try {

//プレビューデータの格納用変数
let previewResult:IMessage["linkData"] = {
//"0":{}
};

//URLの配列分フェッチ、パース処理
for (let index in urls) {

//もしURLが画像用ならここで処理して終了
if (urls[index].match(/(https?:\/\/.*\.(?:png|jpg))/g) !== null) {
previewResult[index] = {
contentType: "image",
mediaType: "image",
url: urls[index],
};
} else {

//Twitter用だったら二重処理
if (urls[index].includes("fxtwitter")) {
//プレビューデータ化処理
const resultForThis = await fetchURLForTwitter(urls[index]);
//挿入 :: ToDo
//previewResult = {
// "0": resultForThis
//};
previewResult[index] = resultForThis;
} else {
//プレビューデータ化処理
const resultForThis = await fetchURL(urls[index]);
//挿入 :: ToDo
previewResult[index] = resultForThis;
}

}
}

//プレビューデータの書き込み処理
return new Promise((resolve)=> {
db.run(
`
UPDATE C` + channelId + ` SET
linkData=?
WHERE messageId=?
`,
[JSON.stringify(previewResult), messageId],
(err:Error) => {
if (err) {
console.log("genLinkPreview :: エラー->", err);
resolve(null);
return;
} else {
resolve(previewResult);
return;
}
}
);
});

} catch(e) {

console.log("getLinkPreview :: エラー->", e);
return null;

}
}

/**
* URLのパース処理
* @param url
*/
async function fetchURL(url:string):Promise<IURLParsed> {
//結果格納用
let resultFetched:IURLParsed = {
contentType: "text/html",
mediaType: "",
url: "",
siteName: "",
title: "",
description: "",
images: [],
favicon: ""
};

//フェッチ、メタデータパース
await ogs({url: url})
.then((data:any) => {
const { error, html, result, response } = data;

//パース
resultFetched = {
contentType: "text/html",
mediaType: result.ogType,
url: result.ogUrl,
siteName: result.ogSiteName,
title: result.ogTitle,
description: result.ogDescription,
images: result.ogImage,
favicon: result.favicon
};
});

//取得できたメタデータを返す
return resultFetched;
}

/**
* twitter用のURLパース処理
* @param url
*/
async function fetchURLForTwitter(url:string) {
//結果格納用
let resultFetched:IURLParsed = {
contentType: "text/html",
mediaType: "",
url: "",
siteName: "",
title: "",
description: "",
images: [],
favicon: ""
};

//一度純粋にHTMLをフェッチ
await fetch(url).then(async (res) => {
//取得データをテキスト化
return await res.text();
}).then(async (text) => {
//console.log("simple fetched json->", text);
//処理したHTMLのテキストからmetaデータ取得する
await ogs({ html: text }).then((data:any) => {
const { error, html, result, response } = data;
//console.log("genLink Preview :: parsed for Better timing->", result);
//パース
resultFetched = {
contentType: "text/html",
mediaType: result.ogType,
url: result.ogUrl,
siteName: result.ogSiteName,
title: result.ogTitle,
description: result.ogDescription,
images: result.ogImage,
favicon: result.favicon
};
});
});

//取得できたメタデータを返す
return resultFetched;
}

0 comments on commit 97c2a8c

Please sign in to comment.