ふたばでスレ検索してSlack通知

めっきり見ることもなくなりましたが、気づいたら検索機能が出来たっぽいので興味がありそうな配信とかだけ通知してもらえたらなと。

SJISサイトのせいかまだ日本語がうまくPOSTできない(文字化ける)作りになってしまったので、配信URLとかせいぜい年末のあれぐらいしか用途が思いつかない。


# コード

```js
// TODO storage管理して一定時間内の同一スレを非通知化 ややできた
// TODO キーワードごとか全体に対して本文regexして無視できる機能
// おそらくSJISなリクエストが(GASでなくHTMLのform再現でもmulti-part形式再現でも)上手くいかないので日本語検索ができない

// そこそこ時間がかかるのでＧＡＳならあまり多くは取得できなさそう。GASで頑張るならこれも非同期にするか実行ごとにローテーションするかして対応
var KEYWORDS = [
  'www.twitch.tv',
  'Qiita'
]
// ↑trailing comma すると以降のインデントが崩れるのでやめときます
var SLACK_INCOMING_URL = 'https://hooks.slack.com/services/'
var IMG_URL = 'https://img.2chan.net/'

// 日本語postが上手くいかないのでcdnもひとまずキャンセル
//eval(UrlFetchApp.fetch('https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/1.0.29/encoding.min.js').getContentText())

function main() {
  attachments = [];
  // TODO: OR検索できるなら負荷軽減のためにfetchを一回に抑える
  for (var i = 0; i < KEYWORDS.length; i++) {
    var obj = getJSONObject(KEYWORDS[i])
    if (!obj || !obj.res) { 
      continue 
    }
    //Logger.log(obj)
    
    ress = Object.keys(obj.res)
    
    for(var j = 0; j < ress.length; j++) {
      number = ress[j]
      
      if (isCheckedThreads(number)) {
        continue
      }
      
      // ひとまずスレ立てを検知することにするので、レスは除外
      res = obj.res[number]
      if (obj.res[number].resto != 0) {
        // レスもとるならリンクはnumberではなくrestoの番号を使えばスレへのリンクになるはず
        continue;
      }
      
      var attachment = makeAttachment(
        IMG_URL + obj.bbscode + '/res/' + number + '.htm',
        IMG_URL.slice(0, -1) + res.thumb.replace(/\\\//g, '/'), // おそらく元画像(.src)の場合はサイズオーバーで非表示の可能性があるのでサムネを
        res.com.replace(/\\\//g, '/').replace(/<br>/g, '\n')
        ); // このセミコロンもインデント対策
      attachments.push(attachment)
    }
  }
  
  if(!attachments.length) {
    return
  }
  sendSlack(attachments)
}

function getJSONObject(keyword) {
  Logger.log(keyword)

  var html = UrlFetchApp.fetch('https://img.2chan.net/b/futaba.php?guid=on',{
    method : 'post',
    payload : {
      keyword: keyword,
      mode:'search',
    },
  }).getContentText('Shift_JIS')
  Logger.log(html)
  var match = html.match(/JSON\.parse\('(.*)'\);<\/script>/)
  Logger.log(match[1].length)
  Logger.log(match[1])
  Logger.log(match[1].slice( -(match[1].length / 2) ) )
  
  if (!match[1]) { return false }
  
  //Logger.log(html.match(/JSON\.parse\('(.*)'\);<\/script>/)[1].replace(/\\\//g, '/'))
  //Logger.log(html.match(/JSON\.parse\('(.*)'\);<\/script>/)[1].replace(/\\\//g, '/').replace(/<br>/g, '\n'))
  
  // \日本語を先に全部置換してしまえば無限ループにする必要もないかもしれない
  while(true) {
    try {
      return JSON.parse(
        match[1]
        .replace(/\\\\"/g, '\\"') // 引用によるfontタグのcolor属性による\\"がパースエラーになるので除外。引用が検索結果にでるかはタイミングとキーワードしだいなので明記
      )
    } catch (e) {
      Logger.log(e)
      // 本文途中に変に/単体が入るパターンがあるようで、bad escaped characterになる。メタ文字でない単体エスケープを検知できればいいのだけれど…頻度しだいで捨て置く。 
      // が、レスを含める関係上、頻度が多いのでつど対処をこころみてみる。
      // SyntaxError: Unexcpected character in string: '\る' といった感じなので、エラー対象のバックスラッシュを取り除いてやる
      
      
      if (!e instanceof SyntaxError) {
        Logger.log('fail instanceof')
        return false
      }
      
      unexcpectedChar = e.message.split("'")[1]
      // 想定していないエラーの場合もとりやめ
      if (!unexcpectedChar.match(/\\./)) {
        console.log(unexcpectedChar + ' fail match')
        return false
      }
      
      reg = new RegExp('\\\\(' + unexcpectedChar.slice(-1) + ')','g')
      match[1] = match[1].replace(reg, '$1')
      //return false
    }
  }
}

function sendSlack(attachments) {
  Logger.log(attachments)
  Logger.log(attachments.map(function (attachment) { return attachment.title}).join('\n'))
  
  var jsonData = {
    username:   'ふたBOT',
    icon_emoji: '',
    channel:    '虹裏',
    text:        attachments.map(function (attachment) { return attachment.title}).join('\n'), // デスクトップ通知で判断しやすいように一行目を集めてみる
    attachments: attachments,
  }
  payload = JSON.stringify(jsonData);
  options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };
  UrlFetchApp.fetch(SLACK_INCOMING_URL, options)
}

function makeAttachment(link, image, text) {
  return {
    title: text.split('\n')[0],
    title_link: link,
//    image_url: image,
    thumb_url: image, // 大きな画像は邪魔な場合に。image_urlとお好みで
    fields: [
      {
        title: '本文',
        value: text,
        short: false,
      }
    ]
  }
}

// 簡易にキューっぽいログで重複通知回避

function isCheckedThreads(number) {
  var properties = PropertiesService.getScriptProperties()
  var MAX_MEMORY = 50
  var threads = properties.getProperty('threads') || ''
  threads = threads.split(',')
  if (threads.indexOf(number) != -1) {
    return true;
  }
  
  threads.push(number)
  if (threads.length > MAX_MEMORY) {
    threads.shift()
  }

  threads = threads.join(',')
  properties.setProperty('threads', threads)
  return false
}
```

HTMLではなくて
`var ret=JSON.parse('{"dispname":0,"bbscode":"b",`
とJSで画面構築しているのでこの部分のJSON文字列を取得してJSONとして扱う。
このパターンはエスケープ処理などまだ自身で処理の決定版を持っていないので普通にHTMLをスクレイピングするほうが助かる。

同パターンをNowで処理していたときはpuppeteerで逃げたけれど、GASではそうも行かない。

***

Twitchはアカウント取ればフォローでメール配信してくれそうなので、もしアカウントとったら不要かな。

作り始めたときは配信開始を自動で通知してくれないと見かけたのだけれど、それはTwitterなどの外部への通知だけで、Twitch内部ではちゃんと通知してくれているのかも。

個人配信もたしかネギっぽいサイトで検知できたはずだから、探し出せれば早々にお払い箱かも。