OSDN Git Service

[feature]死因の詳細な記述をダンプから得てツイートする
[hengband/web.git] / score / tools / tweet_score.py
1 #!/usr/bin/python
2 # -*- coding: utf-8
3 #
4 # OSDNのサーバでrequests_oauthlibを使う方法のメモ
5 #
6 # 1. 環境変数PYTHONUSERBASEでインストール先をWebコンテンツ上の任意のディレクトリに指定し、pipに--userオプションをつけてインストールを実行
7 #    以下は /home/groups/h/he/hengband/htdocs/score/local 以下にインストールする例
8 #
9 #    `$ PYTHONUSERBASE=/home/groups/h/he/hengband/htdocs/score/local pip install --user requests_oauthlib`
10 #
11 # 2. パスは通っているはずなのにシステムにインストールされているrequestsとurllib3が何故か読み込みに失敗するので、上でインストールしたディレクトリにコピーする
12 #
13 #    `$ cp -a /usr/lib/python2.7/dist-packages/requests /usr/lib/python2.7/dist-packages/urllib3 /home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages`
14 #
15 # 3. sys.path.appendで上でインストールしたディレクトリにパスを通してからrequests_oauthlibをimportする
16 #
17 #    `import sys`
18 #    `sys.path.append('/home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages')`
19 #    `import requests_oauthlib`
20 #
21
22 import sys
23 import ConfigParser
24 import sqlite3
25 import gzip
26 import re
27
28
29 def get_config(config_file):
30     ini = ConfigParser.ConfigParser()
31     ini.read(config_file)
32
33     config = {s: {i[0]: i[1] for i in ini.items(s)}
34               for s in ini.sections()}
35
36     return config
37
38
39 def get_score_data(score_db_path, score_id):
40     if score_id is None:
41         cond = 'ORDER BY score_id DESC LIMIT 1'
42     else:
43         cond = 'WHERE score_id = :score_id'
44
45     with sqlite3.connect(score_db_path) as con:
46         con.row_factory = sqlite3.Row
47         sql = '''
48 SELECT
49   *,
50   CASE
51     WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
52     ELSE ''
53   END AS realms_name,
54   CASE
55     WHEN killer = 'ripe' THEN '勝利の後引退'
56     WHEN killer = 'Seppuku' THEN '勝利の後切腹'
57     ELSE killer || 'に殺された'
58   END AS death_reason
59 FROM
60  (SELECT
61     *
62   FROM
63     scores_view
64   {cond})
65 NATURAL LEFT JOIN
66   score_realms
67 NATURAL LEFT JOIN
68   realms
69 GROUP BY
70   score_id
71 '''.format(cond=cond)
72         c = con.execute(sql, {'score_id': score_id})
73         score = c.fetchall()
74
75     return score[0] if len(score) == 1 else None
76
77
78 def get_death_reason_detail(score_id):
79     '''
80     ダンプファイル内から詳細な死因を取得する
81     @param score_id ダンプファイルのスコアID
82     @return 詳細な死因を表す文字列。ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
83     '''
84     subdir = (score_id // 1000) * 1000
85     try:
86         with gzip.open("dumps/{0}/{1}.txt.gz"
87                        .format(subdir, score_id), 'r') as f:
88             dump = f.readlines()
89     except IOError:
90         return None
91
92     # NOTE: 死因の記述は31行目から始まる
93     death_reason = unicode(''.join([l.strip() for l in dump[30:33]]), "UTF-8")
94     match = re.search(u"…あなたは、?(.+)。", death_reason)
95
96     return match.group(1) if match else None
97
98
99 def create_tweet(score_db, score_id):
100     score_data = get_score_data(score_db, options.score_id)
101     if score_data is None:
102         return None
103
104     death_reason_detail = get_death_reason_detail(score_id)
105     if death_reason_detail is None:
106         death_reason_detail = (u"{0} {1}階"
107                                .format(score_data['death_reason'],
108                                        score_data['depth']))
109
110     summary = (u"【新着スコア】{personality_name}{name} Score:{score} "
111                u"{race_name} {class_name}{realms_name} {death_reason_detail}"
112                ).format(death_reason_detail=death_reason_detail, **score_data)
113
114     dump_url = ("https://hengband.osdn.jp/score/show_dump.php?score_id={}"
115                 ).format(score_data['score_id'])
116     screen_url = ("https://hengband.osdn.jp/score/show_screen.php?score_id={}"
117                   ).format(score_data['score_id'])
118
119     tweet = (u"{summary}\n\n"
120              u"dump: {dump_url}\n"
121              u"screen: {screen_url}\n"
122              u"#hengband"
123              ).format(summary=summary,
124                       dump_url=dump_url,
125                       screen_url=screen_url)
126     return tweet
127
128
129 def tweet(oauth, tweet_contents):
130     from requests_oauthlib import OAuth1Session
131     from requests.adapters import HTTPAdapter
132     from requests import codes
133     from logging import getLogger
134     logger = getLogger(__name__)
135
136     twitter = OAuth1Session(**oauth)
137
138     url = "https://api.twitter.com/1.1/statuses/update.json"
139
140     params = {"status": tweet_contents}
141     twitter.mount("https://", HTTPAdapter(max_retries=5))
142
143     logger.info("Posting to Twitter...")
144     logger.info(u"Tweet contents:\n{}".format(tweet_contents))
145     res = twitter.post(url, params=params)
146
147     if res.status_code == codes.ok:
148         logger.info("Success.")
149     else:
150         logger.warning("Failed to post: {code}, {json}"
151                        .format(code=res.status_code, json=res.json()))
152
153
154 def parse_option():
155     from optparse import OptionParser
156     parser = OptionParser()
157     parser.add_option("-s", "--score-id",
158                       type="int", dest="score_id",
159                       help="Target score id.\n"
160                            "If this option is not set, latest score is used.")
161     parser.add_option("-c", "--config",
162                       type="string", dest="config_file",
163                       default="tweet_score.cfg",
164                       help="Configuration INI file [default: %default]")
165     parser.add_option("-l", "--log-file",
166                       type="string", dest="log_file",
167                       help="Logging file name")
168     parser.add_option("-n", "--dry-run",
169                       action="store_true", dest="dry_run",
170                       default=False,
171                       help="Output to stdout instead of posting to Twitter.")
172     return parser.parse_args()
173
174
175 def setup_logger(log_file):
176     from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
177     logger = getLogger(__name__)
178     logger.setLevel(INFO)
179     sh = StreamHandler()
180     logger.addHandler(sh)
181     if log_file:
182         formatter = Formatter('[%(asctime)s] %(message)s')
183         fh = FileHandler(log_file)
184         fh.setFormatter(formatter)
185         logger.addHandler(fh)
186
187
188 if __name__ == '__main__':
189     (options, arg) = parse_option()
190     setup_logger(options.log_file)
191     from logging import getLogger
192     logger = getLogger(__name__)
193
194     try:
195         config = get_config(options.config_file)
196         if 'Python' in config:
197             sys.path.append(config['Python']['local_lib_path'])
198
199         tweet_contents = create_tweet(config['ScoreDB']['path'],
200                                       options.score_id)
201         if tweet_contents is None:
202             logger.warning('No score data found.')
203             sys.exit(1)
204
205         if (options.dry_run):
206             print(tweet_contents)
207         else:
208             tweet(config['TwitterOAuth'], tweet_contents)
209     except Exception:
210         from traceback import format_exc
211         logger.critical(format_exc())