|
/* |
|
前日のはてブRSSを取得してブックマークした記事があればSlackに投稿 |
|
毎朝定期実行する運用を想定 |
|
*/ |
|
|
|
/****** 変数定義 ******/ |
|
|
|
// 環境変数 |
|
const POST_URL = "https://hooks.slack.com/services/xxxxxxxxxxx/xxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx" // SlackのWebhook URL |
|
const DISPLAY_NAME = "山田浩史" // Slackのメッセージに表示する名前 |
|
const HATENA_USERNAME = "hoge" // はてなのユーザ名 |
|
|
|
|
|
// date: RSSフィードを取得する日付けを設定 |
|
const date = new Date() |
|
date.setDate(date.getDate() - 1) // 前日の日付け |
|
|
|
/* |
|
リクエストURLの構造: https://b.hatena.ne.jp/<username>/bookmark.rss?date=<yyyyMMdd> |
|
<username>: はてなアカウント名 |
|
<yyyyMMdd>: yyyyMMddの形式で特定の日にブックマークした記事をクエリ |
|
参考: http://developer.hatena.ne.jp/ja/documents/bookmark/misc/feed |
|
*/ |
|
|
|
// feedURL: RSSフィードのリクエストURL |
|
// Utilities.formatDateでdateからyyyyMMdd形式の文字列を取得 |
|
const feedUrl = "https://b.hatena.ne.jp/" + HATENA_USERNAME + "/bookmark.rss?date=" + Utilities.formatDate(date, "Asia/Tokyo", "yyyyMMdd") |
|
|
|
|
|
/****** 関数定義 ******/ |
|
// RSSフィードから記事情報の配列を整形して返す処理 |
|
function getArticles(url) { |
|
const response = UrlFetchApp.fetch(url) |
|
const xml = response.getContentText() |
|
const document = XmlService.parse(xml) |
|
const root = document.getRootElement() |
|
const rss = XmlService.getNamespace("http://purl.org/rss/1.0/") |
|
const dc = XmlService.getNamespace("dc", "http://purl.org/dc/elements/1.1/") |
|
const hatena = XmlService.getNamespace("hatena", "http://www.hatena.ne.jp/info/xmlns#") // これ使うとはてなブックマークカウンターとか参照できる. |
|
const content = XmlService.getNamespace("content", "http://purl.org/rss/1.0/modules/content/") |
|
const items = root.getChildren("item", rss) |
|
|
|
const articles = [] |
|
|
|
for (const item of items) { |
|
// 記事に登録したタグの処理 |
|
const tags = [] |
|
item.getChildren("subject", dc).forEach((subject) => { |
|
tags.push(subject.getText()) |
|
}) |
|
|
|
// XmlServiceで読めないメタデータを正規表現で抽出 |
|
const contentEncoded = item.getChild("encoded", content).getText() |
|
let thumbUrl = contentEncoded.match(new RegExp(/(?<=img\ssrc=")([^\<\>]*?)"[^\<\>]*?class="entry-image"/)) |
|
let altText = contentEncoded.match(new RegExp(/(?<=alt=")([^\<\>]*?)(?=\s\|).+class="entry-image"/)) |
|
let summary = contentEncoded.match(new RegExp(/\<\/p\>\<p\>(.*?)\<\/p\>\<p\>/)) |
|
thumbUrl = thumbUrl !== null ? thumbUrl[1] : "https://placehold.jp/150x150.png" |
|
thumbUrl = thumbUrl.length > 0 ? thumbUrl : "https://placehold.jp/150x150.png" // 空の文字列がエラーになるので処理 |
|
altText = altText !== null ? altText[1] : "Dummy Image" |
|
altText = altText.length > 0 ? altText : "Dummy Image" |
|
summary = summary !== null && summary[1].length > 0 ? summary[1] : "No summary" |
|
summary = summary.length > 0 ? summary : "No summary" |
|
summary = summary.substring(0, 120) + (summary.length > 120 ? '...' : '') // 文字列を120文字に制限 |
|
|
|
articles.push({ |
|
"title": item.getChild("title", rss).getText(), |
|
"description": item.getChild("description", rss).getText(), |
|
"link": item.getChild("link", rss).getText(), |
|
"date": item.getChild("date", dc).getText(), |
|
"tags": tags, |
|
"thumb_url": thumbUrl, |
|
"alt_text": altText, |
|
"summary": summary |
|
}) |
|
} |
|
return articles |
|
} |
|
|
|
// SlackにPOSTするためのデータとテキスト整形処理 |
|
function createPostData(username, displayName, articles) { |
|
const postData = { |
|
"blocks": [ |
|
{ |
|
"type": "section", |
|
"text": { "type": "mrkdwn", "text": "[" + Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd") + "] <https://b.hatena.ne.jp/" + username + "|" + displayName + ">さんは以下の記事をブックマークしました." } |
|
}, |
|
{ |
|
"type": "context", |
|
"elements": [ |
|
{ |
|
"type": "mrkdwn", |
|
"text": "あなたも記事をシェアしよう => <https://gist.github.com/simics-ja/a52eaf7d702529ef9da3e791771d1cdc|Source Code>" |
|
} |
|
] |
|
} |
|
], |
|
"attachments": [] |
|
} |
|
postData["blocks"].push({ "type": "divider" }) // 記事間の区切り線 |
|
for (article of articles) { |
|
let tagText = "" // タグの文字列 |
|
for (const tag of article["tags"]) { |
|
tagText += "<https://b.hatena.ne.jp/" + username + "/" + tag + "|" + tag + ">, " |
|
} |
|
tagText = tagText.slice(0, tagText.length-2) |
|
postData["attachments"].push({ |
|
"pretext": "*<" + article["link"] + "|" + article["title"] + ">*" + (article["description"].length > 0 ? "\n\n :speech_balloon: " + article["description"] : "") , |
|
}) |
|
postData["attachments"].push( |
|
{ |
|
"blocks": [ |
|
{ |
|
"type": "section", |
|
"text": { "type": "mrkdwn", "text": article["summary"] }, |
|
"accessory": { |
|
"type": "image", |
|
"image_url": article["thumb_url"], |
|
"alt_text": article["alt_text"] |
|
} |
|
}, |
|
{ |
|
"type": "context", |
|
"elements": [{ "type": "mrkdwn", "text": "タグ: " + tagText }] |
|
}, |
|
], |
|
} |
|
) |
|
} |
|
return postData |
|
} |
|
|
|
// データを受け取ってpostUrlにPOSTする処理 |
|
function postMessage(postUrl, postData) { |
|
const options = { |
|
"method" : "post", |
|
"contentType" : "application/json", |
|
"payload" : JSON.stringify(postData), |
|
"muteHttpExceptions": true |
|
} |
|
const response = UrlFetchApp.fetch(postUrl, options) |
|
Logger.log(postUrl + ":response_code>>>" + response.getResponseCode()) |
|
if (response.getResponseCode() == 400) { |
|
throw new Error(response.getContentText()) |
|
} |
|
} |
|
|
|
// エントリーポイント |
|
// プロジェクトのトリガーでこの関数を実行するように設定すればOK |
|
function entry() { |
|
const articles = getArticles(feedUrl) |
|
if (articles.length > 0) { |
|
const postData = createPostData(HATENA_USERNAME, DISPLAY_NAME, articles) |
|
try { |
|
postMessage(POST_URL, postData) |
|
Logger.log("Success") |
|
} catch (e) { |
|
Logger.log(e) |
|
postMessage(POST_URL, { |
|
"blocks": [ |
|
{ |
|
"type": "section", |
|
"text": { "type": "mrkdwn", "text": "[" + Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd") + "] おや? なにかエラーが起きているようです." } |
|
} |
|
] |
|
}) |
|
Logger.log("Send error message") |
|
Logger.log(postData) |
|
} |
|
} else { |
|
Logger.log("No article.") |
|
} |
|
} |