9efffd4a2883a68476d1723befe4f446170b81b3
[mw] / src / mw / clicommands.py
1 ###
2 # mw - VCS-like nonsense for MediaWiki websites
3 # Copyright (C) 2010  Ian Weller <ian@ianweller.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program.  If not, see <http://www.gnu.org/licenses/>.
17 ###
18
19 import codecs
20 import cookielib
21 import getpass
22 import hashlib
23 import mw.metadir
24 from optparse import OptionParser, OptionGroup
25 import os
26 import simplemediawiki
27 import sys
28 import time
29
30
31 class CommandBase(object):
32
33     def __init__(self, name, description, usage=None):
34         self.me = os.path.basename(sys.argv[0])
35         self.description = description
36         if usage is None:
37             usage = '%prog ' + name
38         else:
39             usage = '%%prog %s %s' % (name, usage)
40         self.parser = OptionParser(usage=usage, description=description)
41         self.name = name
42         self.metadir = mw.metadir.Metadir()
43         self.shortcuts = []
44
45     def main(self):
46         (self.options, self.args) = self.parser.parse_args()
47         self.args = self.args[1:] # don't need the first thing
48         self._do_command()
49
50     def _do_command(self):
51         pass
52
53     def _login(self):
54         user = raw_input('Username: ')
55         passwd = getpass.getpass()
56         result = self.api.call({'action': 'login',
57                                 'lgname': user,
58                                 'lgpassword': passwd})
59         if result['login']['result'] == 'Success':
60             # cookies are saved to a file
61             print 'Login successful! (yay)'
62         elif result['login']['result'] == 'NeedToken':
63             print 'Login with token'
64             result = self.api.call({'action': 'login',
65                                     'lgname': user,
66                                     'lgpassword': passwd,
67                                     'lgtoken': result['login']['token']})
68             if result['login']['result'] == 'Success':
69                 print 'Login successful! (yay)'
70             else:
71                 print 'Login failed: %s' % result['login']['result']
72         else:
73             print 'Login failed: %s' % result['login']['result']
74
75     def _die_if_no_init(self):
76         if self.metadir.config is None:
77             print '%s: not a mw repo' % self.me
78             sys.exit(1)
79
80     def _api_setup(self):
81         cookie_file = os.path.join(self.metadir.location, 'cookies')
82         self.api_url = self.metadir.config.get('remote', 'api_url')
83         self.api = simplemediawiki.MediaWiki(self.api_url,
84                                              cookie_file=cookie_file)
85
86
87 class InitCommand(CommandBase):
88
89     def __init__(self):
90         usage = 'API_URL'
91         CommandBase.__init__(self, 'init', 'start a mw repo', usage)
92
93     def _do_command(self):
94         if len(self.args) < 1:
95             self.parser.error('must have URL to remote api.php')
96         elif len(self.args) > 1:
97             self.parser.error('too many arguments')
98         self.metadir.create(self.args[0])
99
100
101 class LoginCommand(CommandBase):
102
103     def __init__(self):
104         CommandBase.__init__(self, 'login', 'authenticate with wiki')
105
106     def _do_command(self):
107         self._die_if_no_init()
108         self._api_setup()
109         self._login()
110
111
112 class LogoutCommand(CommandBase):
113
114     def __init__(self):
115         CommandBase.__init__(self, 'logout', 'forget authentication')
116
117     def _do_command(self):
118         self._die_if_no_init()
119         try:
120             os.unlink(os.path.join(self.metadir.location, 'cookies'))
121         except OSError:
122             pass
123
124 class PullCategoryMembersCommand(CommandBase):
125
126     def __init__(self):
127         usage = '[options] PAGENAME ...'
128         CommandBase.__init__(self, 'pullcat', 'add remote pages to repo belonging to the given category', usage)
129
130     def _do_command(self):
131         self._die_if_no_init()
132         self._api_setup()
133         pages = []
134         pages += self.args
135         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
136 #http://commons.wikimedia.org/w/api.php?action=query&format=xmlfm&generator=categorymembers&gcmlimit=500&gcmtitle=Category:User:Esby
137               data = {
138                       'action': 'query',
139                       'gcmtitle': '|'.join(these_pages),
140                       'generator' : 'categorymembers',
141                       'gcmlimit' : 500
142               }
143         response = self.api.call(data)['query']['pages']
144         for pageid in response.keys():
145           pagename = response[pageid]['title']
146           print pagename
147           pullc = PullCommand()
148           pullc.args = [pagename.encode('utf-8')]
149           pullc._do_command()
150
151
152 class PullCommand(CommandBase):
153
154     def __init__(self):
155         usage = '[options] PAGENAME ...'
156         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
157
158     def _do_command(self):
159         self._die_if_no_init()
160         self._api_setup()
161         pages = []
162         pages += self.args
163         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
164             data = {
165                     'action': 'query',
166                     'titles': '|'.join(these_pages),
167                     'prop': 'info|revisions',
168                     'rvprop': 'ids|flags|timestamp|user|comment|content',
169             }
170             response = self.api.call(data)['query']['pages']
171             for pageid in response.keys():
172                 pagename = response[pageid]['title']
173                 if 'missing' in response[pageid].keys():
174                     print '%s: %s: page does not exist, file not created' % \
175                             (self.me, pagename)
176                     continue
177                 revids = [x['revid'] for x in response[pageid]['revisions']]
178                 revids.sort()
179                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
180                 self.metadir.pages_add_rv(int(pageid),
181                                           response[pageid]['revisions'][0])
182                 filename = mw.metadir.pagename_to_filename(pagename)
183                 with file(os.path.join(self.metadir.root, filename + '.wiki'),
184                           'w') as fd:
185                     data = response[pageid]['revisions'][0]['*']
186                     data = data.encode('utf-8')
187                     fd.write(data)
188
189
190 class StatusCommand(CommandBase):
191
192     def __init__(self):
193         CommandBase.__init__(self, 'status', 'check repo status')
194         self.shortcuts.append('st')
195
196     def _do_command(self):
197         self._die_if_no_init()
198         status = self.metadir.working_dir_status()
199         for file in status:
200             print '%s %s' % (status[file], file)
201
202
203 class DiffCommand(CommandBase):
204
205     def __init__(self):
206         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
207
208     def _do_command(self):
209         self._die_if_no_init()
210         status = self.metadir.working_dir_status()
211         for file in status:
212             if status[file] == 'U':
213                 print self.metadir.diff_rv_to_working(
214                         mw.metadir.filename_to_pagename(file[:-5])),
215
216
217 class CommitCommand(CommandBase):
218
219     def __init__(self):
220         usage = '[FILES]'
221         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
222         self.shortcuts.append('ci')
223         self.parser.add_option('-m', '--message', dest='edit_summary',
224                                help='don\'t prompt for edit summary and '
225                                'use this instead')
226         self.parser.add_option('-b', '--bot', dest='bot', action='store_true',
227                                help='mark actions as a bot (won\'t affect '
228                                'anything if you don\'t have the bot right',
229                                default=False)
230
231     def _do_command(self):
232         self._die_if_no_init()
233         self._api_setup()
234         status = self.metadir.working_dir_status(files=self.args)
235         nothing_to_commit = True
236         for file in status:
237             print '%s %s' % (status[file], file)
238             if status[file] in ['U']:
239                 nothing_to_commit = False
240         if nothing_to_commit:
241             print 'nothing to commit'
242             sys.exit()
243         if self.options.edit_summary == None:
244             print 'Edit summary:',
245             edit_summary = raw_input()
246         else:
247             edit_summary = self.options.edit_summary
248         for file in status:
249             if status[file] in ['U']:
250                 # get edit token
251                 data = {
252                         'action': 'query',
253                         'prop': 'info|revisions',
254                         'intoken': 'edit',
255                         'titles': mw.metadir.filename_to_pagename(file[:-5]),
256                 }
257                 response = self.api.call(data)
258                 pageid = response['query']['pages'].keys()[0]
259                 revid = response['query']['pages'][pageid]['revisions'][0]\
260                         ['revid']
261                 awaitedrevid = self.metadir.pages_get_rv_list({'id': pageid})\
262                         [0]
263                 if revid != awaitedrevid:
264                     print 'warning: edit conflict detected on %s (%s -> %s) ' \
265                             '-- skipping!' % (file, awaitedrevid, revid)
266                     continue
267                 edittoken = response['query']['pages'][pageid]['edittoken']
268                 filename = os.path.join(self.metadir.root, file)
269                 text = codecs.open(filename, 'r', 'utf-8').read()
270                 text = text.encode('utf-8')
271                 if (len(text) != 0) and (text[-1] == '\n'):
272                     text = text[:-1]
273                 md5 = hashlib.md5()
274                 md5.update(text)
275                 textmd5 = md5.hexdigest()
276                 data = {
277                         'action': 'edit',
278                         'title': mw.metadir.filename_to_pagename(file[:-5]),
279                         'token': edittoken,
280                         'text': text,
281                         'md5': textmd5,
282                         'summary': edit_summary,
283                 }
284                 if self.options.bot:
285                     data['bot'] = 'bot'
286                 response = self.api.call(data)
287                 if response['edit']['result'] == 'Success':
288                     if 'nochange' in response['edit']:
289                         print 'warning: no changes detected in %s - ' \
290                                 'skipping and removing ending LF' % file
291                         self.metadir.clean_page(file[:-5])
292                         continue
293                     if response['edit']['oldrevid'] != revid:
294                         print 'warning: edit conflict detected on %s -- ' \
295                                 'skipping!' % file
296                         continue
297                     data = {
298                             'action': 'query',
299                             'revids': response['edit']['newrevid'],
300                             'prop': 'info|revisions',
301                             'rvprop':
302                                     'ids|flags|timestamp|user|comment|content',
303                     }
304                     response = self.api.call(data)['query']['pages']
305                     self.metadir.pages_add_rv(int(pageid),
306                                               response[pageid]['revisions'][0])
307                     print 'waiting 10s before processing the next file'
308                     time.sleep(10)
309                 else:
310                     print 'error: committing %s failed: %s' % \
311                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?