OSDN Git Service

[feature]デイリースコア統計ツイートオプション
[hengband/web.git] / score / tools / tweet_score.py
1 #!/usr/bin/python
2 # -*- coding: utf-8
3
4 '''スコアをツイートする
5
6 OSDNのサーバでrequests_oauthlibを使う方法のメモ
7
8 1. 環境変数PYTHONUSERBASEでインストール先をWebコンテンツ上の任意のディレクトリに指定し、
9    pipに--userオプションをつけてインストールを実行
10    以下は /home/groups/h/he/hengband/htdocs/score/local 以下にインストールする例
11
12    `$ PYTHONUSERBASE=/home/groups/h/he/hengband/htdocs/score/local
13       pip install --user requests_oauthlib`
14
15 2. パスは通っているはずなのにシステムにインストールされているrequestsとurllib3が何故か読み込みに失敗するので、
16    上でインストールしたディレクトリにコピーする
17
18    `$ cp -a /usr/lib/python2.7/dist-packages/requests
19       /usr/lib/python2.7/dist-packages/urllib3
20       /home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages`
21
22 3. sys.path.appendで上でインストールしたディレクトリにパスを通してからrequests_oauthlibをimportする
23
24    `import sys`
25    `sys.path.append('/home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages')`
26    `import requests_oauthlib`
27 '''
28
29 import sys
30 import datetime
31 import sqlite3
32 import gzip
33 import re
34 import config
35
36
37 def get_score_data(score_db_path, score_id):
38     '''DBからスコアデータを取得する。
39
40     Args:
41         score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
42         score_id: 取得するスコアのスコアID。
43             Noneの場合最新のスコアを取得する。
44
45     Returns:
46         取得したスコアのデータを格納した辞書。
47         指定のスコアIDに該当するスコアが見つからない場合None。
48     '''
49     if score_id is None:
50         cond = 'ORDER BY score_id DESC LIMIT 1'
51     else:
52         cond = 'WHERE score_id = :score_id'
53
54     with sqlite3.connect(score_db_path) as con:
55         con.row_factory = sqlite3.Row
56         sql = '''
57 SELECT
58   *,
59   CASE
60     WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
61     ELSE ''
62   END AS realms_name,
63   CASE
64     WHEN killer = 'ripe' THEN '勝利の後引退'
65     WHEN killer = 'Seppuku' THEN '勝利の後切腹'
66     ELSE killer || 'に殺された'
67   END AS death_reason
68 FROM
69  (SELECT
70     *
71   FROM
72     scores_view
73   {cond})
74 NATURAL LEFT JOIN
75   score_realms
76 NATURAL LEFT JOIN
77   realms
78 GROUP BY
79   score_id
80 '''.format(cond=cond)
81         c = con.execute(sql, {'score_id': score_id})
82         score = c.fetchall()
83
84     return score[0] if len(score) == 1 else None
85
86
87 def get_daily_score_stats(score_db_path, year, month, day):
88     '''DBから指定した日付のスコア統計データを得る
89
90     Args:
91         score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
92         year: 指定する年。
93         month: 指定する月。
94         day: 指定する日。
95
96     Returns:
97         取得したスコア統計データを格納した辞書。
98         'total_count': 総スコア件数, 'winner_count': 勝利スコア件数
99     '''
100     with sqlite3.connect(score_db_path) as con:
101         con.row_factory = sqlite3.Row
102         sql = '''
103 SELECT
104   count(*) AS total_count,
105   count(winner = 1 OR NULL) AS winner_count
106 FROM
107   scores
108 WHERE
109   date >= date('{target_date}') AND date < date('{target_date}', '+1 day')
110 '''.format(target_date=datetime.date(year, month, day).isoformat())
111         c = con.execute(sql, {})
112         score = c.fetchall()
113
114     return score[0]
115
116
117 def get_death_reason_detail(score_id):
118     '''ダンプファイル内から詳細な死因を取得する。
119
120     Args:
121         score_id: ダンプファイルのスコアID。
122
123     Returns:
124         詳細な死因を表す文字列。
125         ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
126     '''
127     subdir = (score_id // 1000) * 1000
128     try:
129         with gzip.open("dumps/{0}/{1}.txt.gz"
130                        .format(subdir, score_id), 'r') as f:
131             dump = f.readlines()
132     except IOError:
133         return None
134
135     # NOTE: 死因の記述は31行目から始まる
136     death_reason = unicode(''.join([l.strip() for l in dump[30:33]]), "UTF-8")
137     match = re.search(u"…あなたは、?(.+)。", death_reason)
138
139     return match.group(1) if match else None
140
141
142 def create_tweet(score_db_path, score_id):
143     '''ツイートするメッセージを生成する。
144
145     Args:
146         score_db: スコアデータが格納されているDBへのパスを表す文字列。
147         score_id: 指定するスコアID。Noneの場合最新のスコアを取得する。
148
149     Returns:
150         生成したツイートメッセージ文字列。
151         なんらかの理由により生成できなかった場合None。
152     '''
153     score_data = get_score_data(score_db_path, options.score_id)
154     if score_data is None:
155         return None
156
157     death_reason_detail = get_death_reason_detail(score_data['score_id'])
158     if death_reason_detail is None:
159         death_reason_detail = (u"{0} {1}階"
160                                .format(score_data['death_reason'],
161                                        score_data['depth']))
162
163     summary = (u"【新着スコア】{personality_name}{name} Score:{score}\n"
164                u"{race_name} {class_name}{realms_name}\n"
165                u"{death_reason_detail}"
166                ).format(death_reason_detail=death_reason_detail, **score_data)
167
168     dump_url = ("https://hengband.osdn.jp/score/show_dump.php?score_id={}"
169                 ).format(score_data['score_id'])
170     screen_url = ("https://hengband.osdn.jp/score/show_screen.php?score_id={}"
171                   ).format(score_data['score_id'])
172
173     tweet = (u"{summary}\n\n"
174              u"dump: {dump_url}\n"
175              u"screen: {screen_url}\n"
176              u"#hengband"
177              ).format(summary=summary,
178                       dump_url=dump_url,
179                       screen_url=screen_url)
180     return tweet
181
182
183 def create_daily_stats_tweet(score_db_path, year, month, day):
184     '''デイリースコア統計データのツイートを生成する
185
186     Args:
187         score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
188         year: 指定する年。
189         month: 指定する月。
190         day: 指定する日。
191
192     Returns:
193         生成したツイートメッセージ文字列。
194         なんらかの理由により生成できなかった場合None。
195     '''
196     daily_stats = get_daily_score_stats(score_db_path, year, month, day)
197
198     tweet = (u"{year}年{month}月{day}日のスコア\n"
199              u"全 {total_count} 件, 勝利 {winner_count} 件\n"
200              u"#hengband"
201              ).format(year=year, month=month, day=day,
202                       **daily_stats)
203
204     return tweet
205
206
207 def tweet(oauth, tweet_contents):
208     '''ツイートする。
209
210     Args:
211         oauth: requests_oauthlib.OAuth1Sessionの引数に渡すOAuth認証パラメータ。
212         tweet_contents: ツイートする内容を表す文字列。
213     '''
214     from requests_oauthlib import OAuth1Session
215     from requests.adapters import HTTPAdapter
216     from requests import codes
217     from logging import getLogger
218     logger = getLogger(__name__)
219
220     twitter = OAuth1Session(**oauth)
221
222     url = "https://api.twitter.com/1.1/statuses/update.json"
223
224     params = {"status": tweet_contents}
225     twitter.mount("https://", HTTPAdapter(max_retries=5))
226
227     logger.info("Posting to Twitter...")
228     logger.info(u"Tweet contents:\n{}".format(tweet_contents))
229     res = twitter.post(url, params=params)
230
231     if res.status_code == codes.ok:
232         logger.info("Success.")
233     else:
234         logger.warning("Failed to post: {code}, {json}"
235                        .format(code=res.status_code, json=res.json()))
236
237
238 def parse_option():
239     '''コマンドライン引数をパースする。
240
241     Returns:
242         パースした結果を表す辞書。OptionParser.parse_args()のドキュメント参照。
243     '''
244     from optparse import OptionParser
245     parser = OptionParser()
246     parser.add_option(
247         "-s", "--score-id",
248         type="int", dest="score_id",
249         help="Tweet score with specified id.\n"
250              "When this option and -d are not set, latest score is specified.")
251     parser.add_option(
252         "-d", "--daily-stats",
253         type="string", dest="stats_date",
254         help="Tweet statistics of the score of the specified day.")
255     parser.add_option("-c", "--config",
256                       type="string", dest="config_file",
257                       default="tweet_score.cfg",
258                       help="Configuration INI file [default: %default]")
259     parser.add_option("-l", "--log-file",
260                       type="string", dest="log_file",
261                       help="Logging file name")
262     parser.add_option("-n", "--dry-run",
263                       action="store_true", dest="dry_run",
264                       default=False,
265                       help="Output to stdout instead of posting to Twitter.")
266     return parser.parse_args()
267
268
269 def setup_logger(log_file):
270     '''ロガーをセットアップする。
271
272     Args:
273         log_file: ロガーの出力先ファイル名。
274             Noneの場合、ファイルには出力せず標準エラー出力のみに出力する。
275     '''
276     from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
277     logger = getLogger(__name__)
278     logger.setLevel(INFO)
279     sh = StreamHandler()
280     logger.addHandler(sh)
281     if log_file:
282         formatter = Formatter('[%(asctime)s] %(message)s')
283         fh = FileHandler(log_file)
284         fh.setFormatter(formatter)
285         logger.addHandler(fh)
286
287
288 if __name__ == '__main__':
289     (options, arg) = parse_option()
290     setup_logger(options.log_file)
291     from logging import getLogger
292     logger = getLogger(__name__)
293
294     try:
295         config.parse(options.config_file)
296         if 'Python' in config.config:
297             sys.path.append(config.config['Python']['local_lib_path'])
298
299         if options.stats_date:
300             target_datetime = datetime.datetime.strptime(
301                 options.stats_date, "%Y-%m-%d")
302             tweet_contents = create_daily_stats_tweet(
303                 config.config['ScoreDB']['path'],
304                 target_datetime.year,
305                 target_datetime.month,
306                 target_datetime.day
307             )
308         else:
309             tweet_contents = create_tweet(config.config['ScoreDB']['path'],
310                                           options.score_id)
311
312         if tweet_contents is None:
313             logger.warning('No score data found.')
314             sys.exit(1)
315
316         if (options.dry_run):
317             print(tweet_contents.encode("UTF-8"))
318         else:
319             tweet(config.config['TwitterOAuth'], tweet_contents)
320     except Exception:
321         from traceback import format_exc
322         logger.critical(format_exc())