ãã®è¨äºã¯ãå¼è·å£«ãããã³ã Advent Calendar 2024 ã®14æ¥ç®ã®è¨äºã§ãã
ã¯ã©ã¦ããµã¤ã³ã®ããã³ãã¨ã³ãã¨ã³ã¸ãã¢ã¨ãã¦å
¥ç¤¾ãã¦5å¹´ã® @happylifetaka ã§ãã
å 責ï¼æ¬è¨äºã«è¨è¼ãã¦ããã³ã¼ãã¯ã¢ããã³ãã«ã¬ã³ãã¼ç¨ã«ä½æãããã®ã§ãããå®éã«ä½¿ç¨ãã¦ããã³ã¼ãã¨ã¯ç°ãªãã¾ãã使ç¨ããéã¯ãå¿ è¦ã«å¿ãã¦ä¿®æ£ãã¦ããã ããããã§ãèªå·±è²¬ä»»ã§ãå©ç¨ãã ããã
æ¦è¦
å½ç¤¾ã§ã¯ãSlack ããã£ãããã¼ã«ã¨ãã¦ä½¿ç¨ãã¦ããã社å¡ãèªç±ã«ãã£ã³ãã«ãä½æããã¸ã£ã³ã«ãã¨ã®éè«ãæ¥å ±ãå
±æãã¦ãã¾ãã
ããããæ°ãããã£ã³ãã«ãä½ããããã¨ã社å
ã«åºãç¥ããããå ´åãä½æè
ãèªãéç¥ããªãã¨ãä»ã®ç¤¾å¡ã¯æ°ãããã£ã³ãã«ã®åå¨ã«æ°ã¥ããªãã¨ããåé¡ãããã¾ããã
ãã®åé¡ã解決ããããã«ãSlack ã® API 㨠Google Apps Script (以ä¸ãGAS) ã使ã£ãéç¥ã·ã¹ãã ãå°å
¥ãã¾ããã
ãã®ã·ã¹ãã ã¯ãSlackã®Event API ã使ã£ã¦ãã£ã³ãã«ãä½æãããç¬éãæ¤ç¥ãããã®æ
å ±ã GAS çµç±ã§ç¹å®ã®ãã£ã³ãã«ã«æ稿ãã¾ãã
å°å
¥å¾ã®å°è©±ã§ãããæ°ããå
¥ç¤¾ããã社å¡ãåå ±ãä½æããæã«ããã¾ãã«ãããã«æ¢å社å¡ãåå ããã®ã§é©ããããã¨ããã³ãã³ããã¾ãã
å®è£ æé
Slack App ã®ä½æï¼
Slack API ã®ãµã¤ãã§æ°ããã¢ããªãä½æãã¾ãã
ãCreate New AppãâãFrom scratchããApp Name ã¯é©å½ãªååãå ¥ããå°å ¥ãã Workspace ãé¸æããCreate Appããé¸æãã¾ãã
権éã®è¨å® ãOAuth & Permissionsãã¿ãã§ãChat:Writeãã¨ãchannels:readãã®ã¹ã³ã¼ãã追å ãã¾ãã
ã¢ããªã®ã¤ã³ã¹ãã¼ã«ï¼
ãInstall Appãã¿ãã§ã¯ã¼ã¯ã¹ãã¼ã¹ã«ã¢ããªãã¤ã³ã¹ãã¼ã«ããå¿ è¦ãªæ¨©éã許å¯ãã¾ããGAS ã®è¨å®ï¼
GAS ã®ããã¸ã§ã¯ãã®è¨å®âã¹ã¯ãªããããããã£ã§ãVERIFICATION_TOKENããBOT_TOKENããPOST_CHANNEL_IDããè¨å®ãã¾ãã
ãVERIFICATION_TOKENãã«ãSlack ã®ãBasic Informationãã¿ãã®ãVerification Tokenããè¨å®ãã¾ãã
ãBOT_TOKENãã«ãSlack ã®ãOAuth & Permissionsãã¿ããOAuth Tokensãã®ãBot User OAuth Tokenããè¨å®ãã¾ãã
ãPOST_CHANNEL_IDãã«ãSlack ã®æ稿å
ã®ãã£ã³ãã«åãå³ã¯ãªãã¯ãããã£ã³ãã«è©³ç´°ã表示ãããâä¸é¨ã®ããã£ã³ãã« IDããè¨å®ãã¾ãã
ã³ã¼ãï¼ä»¥ä¸ãã³ã¼ããµã³ãã«ãåç §ï¼ã®å®è£ ã¨ãããã¤ï¼
GAS ã«ã³ã¼ããå®è£ ãããããã¤ãé¸æãã¾ããã¦ã§ãã¢ããªãé¸ã³ãã¢ã¯ã»ã¹ã§ããã¦ã¼ã¶ã¼ãå ¨å¡ã«ããããã¤ãã¾ãã
ã¤ãã³ãã®è¨å®ï¼
Slack ã®ãEvent Subscriptionsãã¿ãã§ãEnable Eventsãããªã³ã«ããGAS ã®ããã㤠URL ãè¨å®ãã¾ãã
ã¤ãã³ããéãããæ¤è¨¼ããã¾ããVerified ã«ãªãã° OK ã§ãã
ãSubscribe to bot eventsãã«ãchannel_createdãã追å ããSave Changesããé¸æãã¾ãã
ãã£ã³ãã«ã¸ã®éç¥è¨å®ï¼
ããã£ã³ãã«è©³ç´°ã表示ãããâãã¤ã³ãã°ã¬ã¼ã·ã§ã³ãã¿ããApp ã®ãã¢ããªã追å ãããã§ãéç¥å ã®ãã£ã³ãã«ã«ã¢ããªã追å ãã¾ãã
éç¥ã®ç¢ºèªï¼
æ°ãããã£ã³ãã«ãä½æãããã¨ãè¨å®ãããã£ã³ãã«ã«éç¥ãæ稿ããããã¨ã確èªãã¾ãã
ããããã£ã³ãã«ã®ãªãã¼ã ãæ¤ç¥ãããå ´åãchannel_renameãã¤ãã³ãã使ãã®ãè¯ãã§ãããã
ã³ã¼ããµã³ãã«
function doPost(req) { const postData = JSON.parse(req.postData.getDataAsString()); const VERIFICATION_TOKEN = PropertiesService.getScriptProperties().getProperty('VERIFICATION_TOKEN'); const BOT_TOKEN = PropertiesService.getScriptProperties().getProperty('BOT_TOKEN'); const POST_CHANNEL_ID = PropertiesService.getScriptProperties().getProperty('POST_CHANNEL_ID'); if (!postData.type || postData.token != VERIFICATION_TOKEN){ throw new Error("Invalid request"); } switch(postData.type) { case 'url_verification': return ContentService.createTextOutput(postData.challenge); case 'event_callback': if (postData.event.type == 'channel_created') { const formData = { 'token': BOT_TOKEN, 'channel': POST_CHANNEL_ID, 'text': 'æ°çãã£ã³ãã« #' + postData.event.channel.name + ' ãä½æããã¾ãã', 'link_names': true, }; const options = { 'method': 'POST', 'payload': formData, }; return UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', options); } break; } }
ç«ã¡ã¯ã ããåé¡ããã®å®è£
ä¸è¨ã®æ¹æ³ã§å®è£ ãèãã¦ãã¾ããããå½æã社å ã»ãã¥ãªãã£ããªã·ã¼ã«ãã GAS ã® URL ãå¤é¨å ¬éã§ããªãã£ããããEvent API ã使ãæ¹æ³ã¯é¸æã§ãã¾ããã§ããã
解決æ¹æ³ã¨ãã¦ãå¤é¨ã¢ã¯ã»ã¹å¯è½ãªãµã¼ãã¼ãç¨æããã該å½ãã¡ã¤ã«ã ãå¤é¨ã¢ã¯ã»ã¹å¯è½ã«ãããªã©ã®ãã¾ãã¾ãªæ¹æ³ãããã¾ãã
ä»åã諸ã
ã®ç¶æ³ãè¸ã¾ãæ¤è¨ã®çµæãåæã¨ãªãã¾ãããå®æçã« API ãå¼ã³åºãã¦ãã£ã³ãã«æ
å ±ãåå¾ããæ¹æ³ã«å¤æ´ãã¾ããã
ããããªå
容ã§ãããèå³ããã人ã¯ã覧ãã ããã
æ°å®è£ æé
- Slack App ã®ä½æï¼ åè¿°ã¨åãæé ã§ã¢ããªãä½æãã¾ãã
- 権éã®è¨å®ï¼ åè¿°ã¨åãã§ãã
- ã¢ããªã®ã¤ã³ã¹ãã¼ã«ï¼ åè¿°ã¨åãã§ãã
- ãã¼ã¯ã³ã®è¨å®ï¼ åè¿°ã¨ã»ã¼åãã§ããVERIFICATION_TOKEN ã®è¨å®ã¯ä¸è¦ã§ãã
- ã¹ãã¬ããã·ã¼ãã®ä½æï¼ ãã£ã³ãã«æ å ±ãä¿åããã¹ãã¬ããã·ã¼ããä½æãã¾ãã
- GAS ã³ã¼ãã®å®è£ ï¼ ã³ã¼ãï¼ä»¥ä¸ãæ°ã³ã¼ããµã³ãã«ãåç §ï¼ã GAS ã§å®è£ ãã¾ãã
- ããããã£ã®è¨å®ï¼ åè¿°ã®ã¹ã¯ãªããããããã£ãè¨å®ãã¾ããVERIFICATION_TOKEN ã®è¨å®ã¯ä¸è¦ã§ãã
- ã³ã¼ãã®å®è¡ï¼ å®è¡ãåä½ã確èªãã¾ãã
- ããªã¬ã¼ã®è¨å®ï¼ å®æçã«ã¹ã¯ãªãããå®è¡ãããããã«ããªã¬ã¼ãè¨å®ãã¾ãã
Slack API ã® conversations.list ã使ç¨ãã¦ãããªãã¯ãã£ã³ãã«ã®ãªã¹ããåå¾ããã¹ãã¬ããã·ã¼ãã«ä¿åãã¾ãã ã¹ãã¬ããã·ã¼ãããã£ã³ãã«æ å ±ã®ä¿æã«ä½¿ããæ¢åã®ãã£ã³ãã«ã¨ã®æ¯è¼ãããã¨ã§ãæ°è¦ããªãã¼ã ããããã£ã³ãã«ãéç¥ãã¾ãã
æ°ã³ã¼ããµã³ãã«
// Slack API ã®BOTãã¼ã¯ã³ãåå¾ const BOT_TOKEN = PropertiesService.getScriptProperties().getProperty('BOT_TOKEN'); const POST_CHANNEL_ID = PropertiesService.getScriptProperties().getProperty('POST_CHANNEL_ID'); /** * ãã£ã³ãã«æ å ±ãåå¾ãã¦å¦çããã¡ã¤ã³é¢æ° */ function getChannel() { const prevChannelNameList = getPrevChannelNameList(); const prevChannelIDList = getPrevChannelIDList(); const result = []; let cursor = undefined; let renamed = false; do { const response = getNewChannelList(cursor); response.channels.forEach(channel => { if (prevChannelNameList.includes(channel.name)) return; // æ¢åã®ãã£ã³ãã«åã¯ã¹ããã if (prevChannelIDList.includes(channel.id)) { renamed = true; // IDãä¸è´ãããååãéãå ´åã¯ãªãã¼ã æ±ã } // æ°ãããã£ã³ãã«ã¾ãã¯ãªãã¼ã ããããã£ã³ãã«ã®æ å ±ã追å result.push({ 'name': channel.name, 'channelId': channel.id, 'createdDate': getJSTDate(channel.created), 'renamed': renamed }); renamed = false; // ãªã»ãã }); cursor = response.next; Utilities.sleep(100); // APIå¼ã³åºãééã空ãã } while (cursor !== ''); if (result.length > 0) { // æ°ãããã£ã³ãã«æ å ±ãã¹ãã¬ããã·ã¼ãã«è¿½å const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); result.forEach(row => { sheet.appendRow([row['name'], row['channelId'], row['createdDate'], row['renamed']]); }); // Slackã«éç¥ postResult(result); } } /** * Slackã«çµæãæ稿ããé¢æ° * @param {Array} result - æ稿ãããã£ã³ãã«ã®æ å ±ãªã¹ã */ function postResult(result) { if (!result || result.length === 0){ return; // çµæã空ãªãå¦çãçµäº } // åãã£ã³ãã«ã®æ å ±ãæ´å½¢ const text = result.map(item => { let line = `#${item['name']} (${item['channelId']})`; if (item['renamed']) { line += `ï¼å¤æ´æ¥æ: ${item['createdDate']} â»ãã£ã³ãã«åå¤æ´`; }else{ line += `ï¼ä½ææ¥æ: ${item['createdDate']}`; } return line; }).join("\n"); // Slackã«ã¡ãã»ã¼ã¸ãæ稿ããããã®ãã©ã¡ã¼ã¿ const url = "https://slack.com/api/chat.postMessage"; const param = { 'token': BOT_TOKEN, 'channel': POST_CHANNEL_ID, 'text': text, 'link_names': 1, // ãã£ã³ãã«åã«ãªã³ã¯ã追å }; const options = { "method": "POST", "payload": param, }; try { // Slackã«ã¡ãã»ã¼ã¸ãæ稿 UrlFetchApp.fetch(url, options); } catch (e) { throw new Error("Slack Post Error: ", e.message); } } /** * Slackãããããªãã¯ãã£ã³ãã«ã®ä¸è¦§ãåå¾ããé¢æ° * @param {string} cursor - ãã¼ã¸ãã¼ã·ã§ã³ã®ããã®ã«ã¼ã½ã« * @returns {Object} - ãã£ã³ãã«ãªã¹ãã¨æ¬¡ã®ã«ã¼ã½ã« */ function getNewChannelList(cursor) { const url = 'https://slack.com/api/conversations.list'; const param = { 'token': BOT_TOKEN, 'cursor': cursor, 'exclude_archived': true, 'limit': 200, 'types': 'public_channel', }; const options = { "method": "GET", "payload": param, }; try { const response = UrlFetchApp.fetch(url, options); const newChannelList = JSON.parse(response.getContentText()); if (!newChannelList.ok) { throw new Error('Slack API Error: ', newChannelList.error || 'Unknown Error'); return { channels: [], next: '' }; } return { channels: newChannelList.channels, next: newChannelList.response_metadata ? newChannelList.response_metadata.next_cursor : '', }; } catch (e) { throw new Error('Get ChannelList Error: ', e.message); return { channels: [], next: '' }; } } /** * Unixã¿ã¤ã ãJSTå½¢å¼ã«å¤æ * @param {number} unixTime - Unixã¿ã¤ã ã¹ã¿ã³ã * @returns {string} JSTå½¢å¼ã®æ¥ä»æåå */ function getJSTDate(unixTime) { return Utilities.formatDate(new Date(unixTime * 1000), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss"); } /** * ã¹ãã¬ããã·ã¼ãããéå»ã®ãã£ã³ãã«ãªã¹ããåå¾ * @param {number} columnNumber - ã¹ãã¬ããã·ã¼ãã®åçªå· * @returns {Array} - åå¾ãããã£ã³ãã«ãªã¹ã */ function getPrevChannelList(columnNumber) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const lastRow = sheet.getLastRow(); // æçµè¡ãåå¾ if (lastRow <= 1) return []; // ãããã¼ãããªãå ´åã¯ç©ºã®é åãè¿ã const dataRange = sheet.getRange(2, columnNumber, lastRow - 1, 1); const values = dataRange.getValues(); return values.map(row => row[0]); // åè¡ãããã¼ã¿ãåãåºãé åãè¿ã } /** * éå»ã«åå¾ãããã£ã³ãã«åã®ãªã¹ããåå¾ * @returns {Array} - ãã£ã³ãã«åã®ãªã¹ã */ function getPrevChannelNameList() { return getPrevChannelList(1); // ãã£ã³ãã«åã¯1åç® } /** * éå»ã«åå¾ãããã£ã³ãã«IDã®ãªã¹ããåå¾ * @returns {Array} - ãã£ã³ãã«IDã®ãªã¹ã */ function getPrevChannelIDList() { return getPrevChannelList(2); // ãã£ã³ãã«IDã¯2åç® } /** * ã¹ãã¬ããã·ã¼ãã®éå»ã®ãã£ã³ãã«æ å ±ãã¯ãªã¢ */ function clearPrevChannelList() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const lastRow = sheet.getLastRow(); const lastColumn = sheet.getLastColumn(); sheet.getRange(2, 1, lastRow - 1, lastColumn).clear(); // 2è¡ç®ä»¥éã®å 容ã¨ã¹ã¿ã¤ã«ãã¯ãªã¢ }
ã¾ã¨ã
Slack ã® Event API ã使ãã¨ãã£ã³ãã«æ°çæ¤ç¥ãç°¡åã«ã§ãã¦ä¾¿å©ã«ãªãã¾ããã
ã¾ããåæã®ããããªè¨äºãéè¦ããã¨èãå
¬éãã¦ã¿ã¾ããã
ï¼Slack ã® Event API ã使ãã®ãå¼·ãæ¨å¥¨ãã¾ãï¼