Skip to content

Instantly share code, notes, and snippets.

@simics-ja
Last active April 13, 2020 18:06
Show Gist options
  • Save simics-ja/a52eaf7d702529ef9da3e791771d1cdc to your computer and use it in GitHub Desktop.
Save simics-ja/a52eaf7d702529ef9da3e791771d1cdc to your computer and use it in GitHub Desktop.
[はてなブックマークをSlackへポストするGoogle App Script] SlackにもRSSアプリあるけど投稿の見た目がごちゃごちゃしてて微妙だった #GAS #Slack

はてなブックマークからSlackに記事をシェアするBotです.
各自がGoogle App Scriptで特定のチャンネルに投稿することを想定してます.
こういうことがやりたくて作ってみました.
雑談のタネにどうぞ.

  1. Google Spreadsheetを新規作成
  2. 「ツール」>「スクリプトエディタ」を開く.
  3. PostArticleFromHatena.jsの内容をひとまずコピペする.
  4. コード中の9行目らへんの環境変数POST_URL,DISPLAY_NAME,HATENA_USERNAMEを自分の環境に合わせる.WebhookのURLは管理者に教えてもらおう.
    • POST_URL: SlackのIncoming Webhookでchannelごとに発行されるURL
    • DISPLAY_NAME: Slackに投稿するときに表示される名前
    • HATENA_USERNAME: はてなアカウントのユーザ名
  5. テストとして実行する関数をentryに設定し,実行ボタンを押す. image
    • 初回の実行なら権限を要求されるので許可する.
      • Chromeだと途中で「このアプリは確認されていません」と表示されるけど,詳細を開いて「安全ではないページに移動」をクリックしてOK. image
    • 前日にブックマークが無ければ,何も投稿されない.
      • すぐテストしたいならひとまず16行目date.setDate(date.getDate() - 1)-10にすれば良い.
  6. 「編集」>「現在のプロジェクトのトリガー」を開く.
  7. 「トリガーを追加」ボタンを押す.
  8. 以下のように設定すれば定期実行される.時間はお好きな時間に.

image

/*
前日のはてブ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.")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment