OSDN Git Service

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