AOJの毎日solved数投稿bot作った


前の日曜日に行われたICPC-模擬地区予選のオンサイトの懇親会で話している時に、個人練習がおろそかになりそうなら、AOJをやっているかどうかのbotを作ればいいのではという話になったので、作ってみました。


botを作るのもそうでしたが、自分自身のAOJの問題一覧やsolved一覧などもなんらかの形式で持っておきたかったというのもあったので、最近はまっているgoogle apps script(GAS)でそれぞれやってみました。

まずtwitter上での表示としては以下のようになっています。

 

やったこととしては、AOJのAPIで問題一覧取得、指定したidで解いた問題に印を付ける、userAPIで取得できる情報を表におこすといったものです。その後、昔別に作ったtwitter投稿部分を持ってきて、前回と今回のsolvedの差を投稿する関数を作成、その関数を毎日0時頃実行のトリガーに設定して、毎日投稿を実現しています。

GASで行った理由としては以下の理由からです。

  1. webからの情報を自由に操作できる上に、その情報をdriveのドキュメント等に簡単に書きだすことができる。
  2. 実行用のサーバを用意せずに、cron的な時間指定の実行ができる
  3. 最近、ハマっているため

1つめの理由は前述のように自分のAOJのデータを操作しやすく、視認しやすいデータ形式で持ちたかったためです。

2つめは、定期実行するためのサーバを用意しなくても、googleの強固なサーバで実行してくれるため基本的に止まる心配や管理がほとんどいらないという点です。そこまで信頼性の要求されるシステムではありませんが、逆に自鯖でやった場合、存在を忘れていてサーバの設定を変えたせいで動かなくなった、データを消した等も考えれるので、そういった心配も不要になります。

今のところ自分用に作ったので、このサービス自体を公開する予定は特にないですが、要望がある程度あれば対応してみます。

とりあえず使ってみたいというかたは、以下にコードをすべてはっているので、ご自身のgoogle Drive上でスクリプトを貼り付けて実行してみてください。

なお、スクリプト以外に必要になるのは、twitter-developの登録(CONSUMER_KEYとCONSUMER_SECRETの取得)、それと情報保存用のgoogle スプレットシートのシートid(シートのURLのkey以下)です。

この3つの項目をコードの最初に書いてあるユーザプロパティ名で保存すれば実行可能です。

綺麗にコードを共有する方法が思い浮かばなかったので、ひとまず以下にコードをベタ貼りしています。
途中、AOJ問題一覧と正解した問題を取得して、表形式で整形するものとかも脱線して作っていたので、botとしては不要な部分が多くなっています。
しかも、オンサイトのあと、帰宅してそのままとりあえず動かそうの精神と勢いだけで書いたため、いろいろと無駄やバグも多いと思います。使用時は自己責任でお願いします。

//データを保存するスプレッドシートのidを保存
//ユーザプロパティには以下の3つの値を指定する必要あり
///SHEET_ID:データを保存するスプレッドシートのid
///CONSUMER_KEY:twitter-OauthのConsumer key
///CONSUMER_SECRET:twitter-OauthのConsumer secret

var ss =  SpreadsheetApp.openById(UserProperties.getProperty('SHEET_ID'));

//引数で渡したidのユーザのsubmit数を返す関数
//返す値は、setUserDataでシートに保存した値
//項目が見つからない場合-1を返す
function getSubmit(id){
  var sheet = ss.getSheetByName('user_list');
  if(sheet == null)
  {
    sheet = ss.insertSheet('user_list');
  }
  var Row = 1;
  var lastColumn = sheet.getLastColumn(); 
  var lastRow = sheet.getLastRow();  
  var Column;
  for (Column = 1; Column < lastColumn ; Column++) {     if(sheet.getRange(1,Column + 1).getValue()==id)       break;   }   if(Column > lastColumn)
    return -1;

  for (Row = 1; Row < lastRow ; Row++) {
    if(sheet.getRange(Row + 1,1).getValue()=="solved")
    {
      return sheet.getRange(Row + 1,Column + 1).getValue();
    }
  }
  return -1;

}

//スプレッドシート内の[user_list]シートに記載されている
//User一覧を配列で返す
function getUserList(){
  var sheet = ss.getSheetByName('user_list');
    if(sheet == null)
  {
   sheet = ss.insertSheet('user_list');
  }
  var lastColumn = sheet.getLastColumn(); 

  var UserList = new Array();
  for (var Column = 1; Column < lastColumn ; Column++) {
    UserList.push(sheet.getRange(1,Column + 1).getValue());
  }

  return UserList;

}

///毎日実行のtwitter投稿用関数
///基本的にこの関数を毎日実行のトリガーに指定して動作させる
///[user_list]シートに記載されているidについて実行前のsolvedと
///現在のsolvedの値を比較してその値をtwitterに投稿する
function DailyDiff(){
  var Users = getUserList();

  for (var i = 0; i < Users.length; i++) {   
    var yesterdaySubmit=getSubmit(Users[i]);
    setUserData(Users[i]);
    var todaySubmit=getSubmit(Users[i]);
    if(yesterdaySubmit!=-1 && todaySubmit!=-1)
    {
      var text = Users[i]+': Aizu Online Judgeでの昨日のsolved数は"'+(todaySubmit-yesterdaySubmit)+'"です。';
      tweetInitialize();
     // Logger.log(text);
      tweetPost(text);
    }
    Logger.log(yesterdaySubmit+":"+todaySubmit);

  }
}

/*
引数で指定したidのUserのデータを
[user_list]シートに記載する
すでにidが記載されている場合、その行に記載、
記載がない場合は、末尾に追記される
*/
function setUserData(id){
  var sheet = ss.getSheetByName('user_list');
  if(sheet == null)
  {
   sheet = ss.insertSheet('user_list');
  }
  var Row = 1;
  var lastColumn = sheet.getLastColumn();  
  var Column;
  for (var Column = 1; Column < lastColumn ; Column++) {
    if(sheet.getRange(1,Column + 1).getValue()==id)
      break;
  }

  var response = UrlFetchApp.fetch("http://judge.u-aizu.ac.jp/onlinejudge/webservice/user?id="+id);
  var xml = response.getContentText();
  var document = XmlService.parse(xml);
  var root = document.getRootElement();
  var list = root.getChildren();
  for (var i = 0; i < list.length; i++) {  

    switch (list[i].getName()) {
      case "status":
        var node = list[i].getChildren();
        for (var j = 0; j < node.length; j++) {  
          sheet.getRange(Row,1).setValue(node[j].getName());
          sheet.getRange(Row,Column + 1).setValue(node[j].getValue());
          Row++;
        }
        break;
      case "problem_score":
        var node = list[i].getChildren();
        for (var j = 0; j < node.length; j++) {  
          sheet.getRange(Row,1).setValue(node[j].getName()+":"+node[j].getAttribute('category').getValue());
          sheet.getRange(Row,Column + 1).setValue(node[j].getValue());
          Row++;
        }

      case "solved_list":break;

      default :
        sheet.getRange(Row,1).setValue(list[i].getName());
        sheet.getRange(Row,Column + 1).setValue(list[i].getValue());
        Row++;
        break;
    }
  }
}

/*
AOJの問題一覧(id,name,memlimit,timelimit)を取得して
スプレッドシート内の[user_list]シートに記載する
歯抜けのidをあとから取得するために、毎回id:1から
書きなおしている
*/
function getAOJAllProgramList(){
  var min=0;
  var max=25;
  var row = 2;
  for(var vol=min;vol<=max;vol++)
  {
    var response = UrlFetchApp.fetch("http://judge.u-aizu.ac.jp/onlinejudge/webservice/problem_list?volume="+vol);
    var sheet = ss.getSheetByName('program_list');
      if(sheet == null)
  {
   sheet = ss.insertSheet('program_list');
  }
    var xml = response.getContentText();
    var document = XmlService.parse(xml);
    var root = document.getRootElement();
    var items = root.getChildren();
    for (var i = 0; i < items.length; i++) {
      var childs =items[i].getChildren();
      for (var j = 0; j < childs.length; j++) {
        var el = childs[j];
        if(i==0)
        {
          sheet.getRange(1,j+1).setValue(el.getName());
        }
        sheet.getRange(row,j+1).setValue(el.getValue());
      }
      row++;
    }        
  }
}

/*
usersolvedテスト用関数
実行前にidを [program_list]1行目のどこかの行に記載する必要あり
*/
function TestsetUsersolved(id){
  id ="ik11235";
  setUsersolved(id);
}

/*
idに記載されているユーザ名に関して
[program_list]に記載のあるidの問題が解けているかどうかを判定
解けている場合そのidの行、Userの列のセルに"○"を記載する
実行前にidを [program_list]1行目のどこかの行に記載する必要あり
注:1つのセルの値を取るのに0.2~0.5秒前後かかるため、1200問以上あるAOJで
1人実行するだけで数分時間がかかる.要改善
*/
function setUsersolved(id){
  var sheet = ss.getSheetByName('program_list');
  if(sheet == null)
  {
    sheet = ss.insertSheet('program_list');
  }  
  var lastRow = sheet.getLastRow();
  var lastColumn = sheet.getLastColumn();  
  var row = 1;
  var Column;
  for (Column = 0; Column < lastColumn ; Column++) {
    if(sheet.getRange(1,Column + 1).getValue()==id)
      break;
  }

  var list = getAOJUsersolvedList(id);
  for (var i = 0; i < list.length; i++) {   
    for (; row < lastRow ; row++) {
      if(sheet.getRange(row + 1,1).getValue()==list[i])
      {
        sheet.getRange(row + 1,Column + 1).setValue("○");
        break;
      }
    }
  }
}

/*
注:未完成
setUsersolvedの実行時間の問題により、数人になっただけで、
GASの実行時間制限で止まる

[program_list]に記載のある全ユーザに対して
usersolvedテスト用関数
idに記載されているユーザ名に関して
[program_list]に記載のあるidの問題が解けているかどうかを判定
解けている場合そのidの行、Userの列のセルに"○"を記載する
実行前にidを [program_list]1行目のどこかの行に記載する必要あり
*/
function setAllUsersolved(){
  var sheet = ss.getSheetByName('program_list');
  if(sheet == null)
  {
    sheet = ss.insertSheet('program_list');
  }  
  var firstColumn = 6;
  var lastColumn = sheet.getLastColumn();  
  var lastRow = sheet.getLastRow();

  for (var Column = firstColumn; Column < lastColumn ; Column++) {    
    id =sheet.getRange(1,Column).getValue();
    var row = 1;
    var list = getAOJUsersolvedList(id);
    for (var i = 0; i < list.length; i++) {   
    for (; row < lastRow ; row++) {
      if(sheet.getRange(row + 1,1).getValue()==list[i])
      {
        sheet.getRange(row + 1,Column + 1).setValue("○");
        break;
      }
    }
    }
  }
}

/*
idで指定されたUserのsolvedした問題idを
配列で返す関数
*/
function getAOJUsersolvedList(id){
  var solvedList = new Array();

  var response = UrlFetchApp.fetch("http://judge.u-aizu.ac.jp/onlinejudge/webservice/user?id="+id);
    var sheet = ss.getSheetByName('program_list');
      if(sheet == null)
  {
   sheet = ss.insertSheet('program_list');
  }  var xml = response.getContentText();
  var document = XmlService.parse(xml);
  var root = document.getRootElement();
  var list = root.getChild("solved_list").getChildren();
  for (var i = 0; i < list.length; i++) {   
    var id = list[i].getChild("id").getValue();
    solvedList.push(id);
  }
  return solvedList;
}

//tweet部分テスト用関数
function testTweet(){
  var text = 'New apps test';
  tweetInitialize();
  tweetPost(text);
}

//twitter認証関係
function tweetInitialize(){
  var oAuthConfig = UrlFetchApp.addOAuthService("twitter");
  oAuthConfig.setRequestTokenUrl("https://api.twitter.com/oauth/request_token");
  oAuthConfig.setAuthorizationUrl("https://api.twitter.com/oauth/authorize");
  oAuthConfig.setAccessTokenUrl("https://api.twitter.com/oauth/access_token");
  oAuthConfig.setConsumerKey(UserProperties.getProperty('CONSUMER_KEY'));
  oAuthConfig.setConsumerSecret(UserProperties.getProperty('CONSUMER_SECRET'));
}

//twitter投稿関数
/*
引数で渡されたテキストをtwitterに投げる
*/
function tweetPost(text){
 var options =
  {
    "oAuthServiceName" : "twitter",
    "oAuthUseToken" : "always",
    "method" : "POST"
  };

  var encodedTweet = encodeURIComponent(text);
  var result = UrlFetchApp.fetch("http://api.twitter.com/1.1/statuses/update.json?status=" + encodedTweet, options);
  var res  = Utilities.jsonParse(result.getContentText());
  Logger.log(res);
}

botとして必要な部分だけを取り出した場合は、以下のようになります。


//データを保存するスプレッドシートのidを保存
//ユーザプロパティには以下の3つの値を指定する必要あり
///SHEET_ID:データを保存するスプレッドシートのid
///CONSUMER_KEY:twitter-OauthのConsumer key
///CONSUMER_SECRET:twitter-OauthのConsumer secret

var ss =  SpreadsheetApp.openById(UserProperties.getProperty('SHEET_ID'));

//引数で渡したidのユーザのsubmit数を返す関数
//返す値は、setUserDataでシートに保存した値
//項目が見つからない場合-1を返す
function getSubmit(id){
  var sheet = ss.getSheetByName('user_list');
  if(sheet == null)
  {
    sheet = ss.insertSheet('user_list');
  }
  var Row = 1;
  var lastColumn = sheet.getLastColumn(); 
  var lastRow = sheet.getLastRow();  
  var Column;
  for (Column = 1; Column < lastColumn ; Column++) {
    if(sheet.getRange(1,Column + 1).getValue()==id)
      break;
  }
  if(Column > lastColumn)
    return -1;
  
  for (Row = 1; Row < lastRow ; Row++) {
    if(sheet.getRange(Row + 1,1).getValue()=="solved")
    {
      return sheet.getRange(Row + 1,Column + 1).getValue();
    }
  }
  return -1;
  
  
}

//スプレッドシート内の[user_list]シートに記載されている
//User一覧を配列で返す
function getUserList(){
  var sheet = ss.getSheetByName('user_list');
    if(sheet == null)
  {
   sheet = ss.insertSheet('user_list');
  }
  var lastColumn = sheet.getLastColumn(); 

  var UserList = new Array();
  for (var Column = 1; Column < lastColumn ; Column++) {
    UserList.push(sheet.getRange(1,Column + 1).getValue());
  }
  
  return UserList;
  
}


///毎日実行のtwitter投稿用関数
///基本的にこの関数を毎日実行のトリガーに指定して動作させる
///[user_list]シートに記載されているidについて実行前のsolvedと
///現在のsolvedの値を比較してその値をtwitterに投稿する
function DailyDiff(){
  var Users = getUserList();
  
  for (var i = 0; i < Users.length; i++) {   
    var yesterdaySubmit=getSubmit(Users[i]);
    setUserData(Users[i]);
    var todaySubmit=getSubmit(Users[i]);
    if(yesterdaySubmit!=-1 && todaySubmit!=-1)
    {
      var text = Users[i]+': Aizu Online Judgeでの昨日のsolved数は"'+(todaySubmit-yesterdaySubmit)+'"です。';
      tweetInitialize();
     // Logger.log(text);
      tweetPost(text);
    }
    Logger.log(yesterdaySubmit+":"+todaySubmit);
    
  }
}



//tweet部分テスト用関数
function testTweet(){
  var text = 'New apps test';
  tweetInitialize();
  tweetPost(text);
}

//twitter認証関係
function tweetInitialize(){
  var oAuthConfig = UrlFetchApp.addOAuthService("twitter");
  oAuthConfig.setRequestTokenUrl("https://api.twitter.com/oauth/request_token");
  oAuthConfig.setAuthorizationUrl("https://api.twitter.com/oauth/authorize");
  oAuthConfig.setAccessTokenUrl("https://api.twitter.com/oauth/access_token");
  oAuthConfig.setConsumerKey(UserProperties.getProperty('CONSUMER_KEY'));
  oAuthConfig.setConsumerSecret(UserProperties.getProperty('CONSUMER_SECRET'));
}


//twitter投稿関数
/*
引数で渡されたテキストをtwitterに投げる
*/
function tweetPost(text){
 var options =
  {
    "oAuthServiceName" : "twitter",
    "oAuthUseToken" : "always",
    "method" : "POST"
  };
  
  var encodedTweet = encodeURIComponent(text);
  var result = UrlFetchApp.fetch("http://api.twitter.com/1.1/statuses/update.json?status=" + encodedTweet, options);
  var res  = Utilities.jsonParse(result.getContentText());
  Logger.log(res);
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください