OSDN Git Service

[modify]種族・職業表示前、死因表示前に改行する
[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 sqlite3
31 import gzip
32 import re
33 import config
34
35
36 def get_score_data(score_db_path, score_id):
37     '''DBからスコアデータを取得する。
38
39     Args:
40         score_db_path: スコアデータが格納されているDBへのパスを表す文字列。
41         score_id: 取得するスコアのスコアID。
42             Noneの場合最新のスコアを取得する。
43
44     Returns:
45         取得したスコアのデータを格納した辞書。
46         指定のスコアIDに該当するスコアが見つからない場合None。
47     '''
48     if score_id is None:
49         cond = 'ORDER BY score_id DESC LIMIT 1'
50     else:
51         cond = 'WHERE score_id = :score_id'
52
53     with sqlite3.connect(score_db_path) as con:
54         con.row_factory = sqlite3.Row
55         sql = '''
56 SELECT
57   *,
58   CASE
59     WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
60     ELSE ''
61   END AS realms_name,
62   CASE
63     WHEN killer = 'ripe' THEN '勝利の後引退'
64     WHEN killer = 'Seppuku' THEN '勝利の後切腹'
65     ELSE killer || 'に殺された'
66   END AS death_reason
67 FROM
68  (SELECT
69     *
70   FROM
71     scores_view
72   {cond})
73 NATURAL LEFT JOIN
74   score_realms
75 NATURAL LEFT JOIN
76   realms
77 GROUP BY
78   score_id
79 '''.format(cond=cond)
80         c = con.execute(sql, {'score_id': score_id})
81         score = c.fetchall()
82
83     return score[0] if len(score) == 1 else None
84
85
86 def get_death_reason_detail(score_id):
87     '''ダンプファイル内から詳細な死因を取得する。
88
89     Args:
90         score_id: ダンプファイルのスコアID。
91
92     Returns:
93         詳細な死因を表す文字列。
94         ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
95     '''
96     subdir = (score_id // 1000) * 1000
97     try:
98         with gzip.open("dumps/{0}/{1}.txt.gz"
99                        .format(subdir, score_id), 'r') as f:
100             dump = f.readlines()
101     except IOError:
102         return None
103
104     # NOTE: 死因の記述は31行目から始まる
105     death_reason = unicode(''.join([l.strip() for l in dump[30:33]]), "UTF-8")
106     match = re.search(u"…あなたは、?(.+)。", death_reason)
107
108     return match.group(1) if match else None
109
110
111 def create_tweet(score_db_path, score_id):
112     '''ツイートするメッセージを生成する。
113
114     Args:
115         score_db: スコアデータが格納されているDBへのパスを表す文字列。
116         score_id: 指定するスコアID。
117
118     Returns:
119         生成したツイートメッセージ文字列。
120         なんらかの理由により生成できなかった場合None。
121     '''
122     score_data = get_score_data(score_db_path, options.score_id)
123     if score_data is None:
124         return None
125
126     death_reason_detail = get_death_reason_detail(score_id)
127     if death_reason_detail is None:
128         death_reason_detail = (u"{0} {1}階"
129                                .format(score_data['death_reason'],
130                                        score_data['depth']))
131
132     summary = (u"【新着スコア】{personality_name}{name} Score:{score}\n"
133                u"{race_name} {class_name}{realms_name}\n"
134                u"{death_reason_detail}"
135                ).format(death_reason_detail=death_reason_detail, **score_data)
136
137     dump_url = ("https://hengband.osdn.jp/score/show_dump.php?score_id={}"
138                 ).format(score_data['score_id'])
139     screen_url = ("https://hengband.osdn.jp/score/show_screen.php?score_id={}"
140                   ).format(score_data['score_id'])
141
142     tweet = (u"{summary}\n\n"
143              u"dump: {dump_url}\n"
144              u"screen: {screen_url}\n"
145              u"#hengband"
146              ).format(summary=summary,
147                       dump_url=dump_url,
148                       screen_url=screen_url)
149     return tweet
150
151
152 def tweet(oauth, tweet_contents):
153     '''ツイートする。
154
155     Args:
156         oauth: requests_oauthlib.OAuth1Sessionの引数に渡すOAuth認証パラメータ。
157         tweet_contents: ツイートする内容を表す文字列。
158     '''
159     from requests_oauthlib import OAuth1Session
160     from requests.adapters import HTTPAdapter
161     from requests import codes
162     from logging import getLogger
163     logger = getLogger(__name__)
164
165     twitter = OAuth1Session(**oauth)
166
167     url = "https://api.twitter.com/1.1/statuses/update.json"
168
169     params = {"status": tweet_contents}
170     twitter.mount("https://", HTTPAdapter(max_retries=5))
171
172     logger.info("Posting to Twitter...")
173     logger.info(u"Tweet contents:\n{}".format(tweet_contents))
174     res = twitter.post(url, params=params)
175
176     if res.status_code == codes.ok:
177         logger.info("Success.")
178     else:
179         logger.warning("Failed to post: {code}, {json}"
180                        .format(code=res.status_code, json=res.json()))
181
182
183 def parse_option():
184     '''コマンドライン引数をパースする。
185
186     Returns:
187         パースした結果を表す辞書。OptionParser.parse_args()のドキュメント参照。
188     '''
189     from optparse import OptionParser
190     parser = OptionParser()
191     parser.add_option("-s", "--score-id",
192                       type="int", dest="score_id",
193                       help="Target score id.\n"
194                            "If this option is not set, latest score is used.")
195     parser.add_option("-c", "--config",
196                       type="string", dest="config_file",
197                       default="tweet_score.cfg",
198                       help="Configuration INI file [default: %default]")
199     parser.add_option("-l", "--log-file",
200                       type="string", dest="log_file",
201                       help="Logging file name")
202     parser.add_option("-n", "--dry-run",
203                       action="store_true", dest="dry_run",
204                       default=False,
205                       help="Output to stdout instead of posting to Twitter.")
206     return parser.parse_args()
207
208
209 def setup_logger(log_file):
210     '''ロガーをセットアップする。
211
212     Args:
213         log_file: ロガーの出力先ファイル名。
214             Noneの場合、ファイルには出力せず標準エラー出力のみに出力する。
215     '''
216     from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
217     logger = getLogger(__name__)
218     logger.setLevel(INFO)
219     sh = StreamHandler()
220     logger.addHandler(sh)
221     if log_file:
222         formatter = Formatter('[%(asctime)s] %(message)s')
223         fh = FileHandler(log_file)
224         fh.setFormatter(formatter)
225         logger.addHandler(fh)
226
227
228 if __name__ == '__main__':
229     (options, arg) = parse_option()
230     setup_logger(options.log_file)
231     from logging import getLogger
232     logger = getLogger(__name__)
233
234     try:
235         config.parse(options.config_file)
236         if 'Python' in config.config:
237             sys.path.append(config.config['Python']['local_lib_path'])
238
239         tweet_contents = create_tweet(config.config['ScoreDB']['path'],
240                                       options.score_id)
241         if tweet_contents is None:
242             logger.warning('No score data found.')
243             sys.exit(1)
244
245         if (options.dry_run):
246             print(tweet_contents)
247         else:
248             tweet(config.config['TwitterOAuth'], tweet_contents)
249     except Exception:
250         from traceback import format_exc
251         logger.critical(format_exc())