4 # OSDNのサーバでrequests_oauthlibを使う方法のメモ
6 # 1. 環境変数PYTHONUSERBASEでインストール先をWebコンテンツ上の任意のディレクトリに指定し、pipに--userオプションをつけてインストールを実行
7 # 以下は /home/groups/h/he/hengband/htdocs/score/local 以下にインストールする例
9 # `$ PYTHONUSERBASE=/home/groups/h/he/hengband/htdocs/score/local pip install --user requests_oauthlib`
11 # 2. パスは通っているはずなのにシステムにインストールされているrequestsとurllib3が何故か読み込みに失敗するので、上でインストールしたディレクトリにコピーする
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`
15 # 3. sys.path.appendで上でインストールしたディレクトリにパスを通してからrequests_oauthlibをimportする
18 # `sys.path.append('/home/groups/h/he/hengband/htdocs/score/local/lib/python2.7/site-packages')`
19 # `import requests_oauthlib`
29 def get_config(config_file):
30 ini = ConfigParser.ConfigParser()
33 config = {s: {i[0]: i[1] for i in ini.items(s)}
34 for s in ini.sections()}
39 def get_score_data(score_db_path, score_id):
41 cond = 'ORDER BY score_id DESC LIMIT 1'
43 cond = 'WHERE score_id = :score_id'
45 with sqlite3.connect(score_db_path) as con:
46 con.row_factory = sqlite3.Row
51 WHEN realm_id IS NOT NULL THEN '(' || group_concat(realm_name) || ')'
55 WHEN killer = 'ripe' THEN '勝利の後引退'
56 WHEN killer = 'Seppuku' THEN '勝利の後切腹'
57 ELSE killer || 'に殺された'
72 c = con.execute(sql, {'score_id': score_id})
75 return score[0] if len(score) == 1 else None
78 def get_death_reason_detail(score_id):
81 @param score_id ダンプファイルのスコアID
82 @return 詳細な死因を表す文字列。ダンプファイルが無い、もしくは詳細な死因が見つからなかった場合None。
84 subdir = (score_id // 1000) * 1000
86 with gzip.open("dumps/{0}/{1}.txt.gz"
87 .format(subdir, score_id), 'r') as f:
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)
96 return match.group(1) if match else None
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:
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']))
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)
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'])
119 tweet = (u"{summary}\n\n"
120 u"dump: {dump_url}\n"
121 u"screen: {screen_url}\n"
123 ).format(summary=summary,
125 screen_url=screen_url)
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__)
136 twitter = OAuth1Session(**oauth)
138 url = "https://api.twitter.com/1.1/statuses/update.json"
140 params = {"status": tweet_contents}
141 twitter.mount("https://", HTTPAdapter(max_retries=5))
143 logger.info("Posting to Twitter...")
144 logger.info(u"Tweet contents:\n{}".format(tweet_contents))
145 res = twitter.post(url, params=params)
147 if res.status_code == codes.ok:
148 logger.info("Success.")
150 logger.warning("Failed to post: {code}, {json}"
151 .format(code=res.status_code, json=res.json()))
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",
171 help="Output to stdout instead of posting to Twitter.")
172 return parser.parse_args()
175 def setup_logger(log_file):
176 from logging import getLogger, StreamHandler, FileHandler, Formatter, INFO
177 logger = getLogger(__name__)
178 logger.setLevel(INFO)
180 logger.addHandler(sh)
182 formatter = Formatter('[%(asctime)s] %(message)s')
183 fh = FileHandler(log_file)
184 fh.setFormatter(formatter)
185 logger.addHandler(fh)
188 if __name__ == '__main__':
189 (options, arg) = parse_option()
190 setup_logger(options.log_file)
191 from logging import getLogger
192 logger = getLogger(__name__)
195 config = get_config(options.config_file)
196 if 'Python' in config:
197 sys.path.append(config['Python']['local_lib_path'])
199 tweet_contents = create_tweet(config['ScoreDB']['path'],
201 if tweet_contents is None:
202 logger.warning('No score data found.')
205 if (options.dry_run):
206 print(tweet_contents)
208 tweet(config['TwitterOAuth'], tweet_contents)
210 from traceback import format_exc
211 logger.critical(format_exc())