Strip out our API and use simplemediawiki
[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
29
30 class CommandBase(object):
31
32     def __init__(self, name, description, usage=None):
33         self.me = os.path.basename(sys.argv[0])
34         self.description = description
35         if usage is None:
36             usage = '%prog ' + name
37         else:
38             usage = '%%prog %s %s' % (name, usage)
39         self.parser = OptionParser(usage=usage, description=description)
40         self.name = name
41         self.metadir = mw.metadir.Metadir()
42         self.shortcuts = []
43
44     def main(self):
45         (self.options, self.args) = self.parser.parse_args()
46         self.args = self.args[1:] # don't need the first thing
47         self._do_command()
48
49     def _do_command(self):
50         pass
51
52     def _login(self):
53         user = raw_input('Username: ')
54         passwd = getpass.getpass()
55         result = self.api.call({'action': 'login',
56                                 'lgname': user,
57                                 'lgpassword': passwd})
58         if result['login']['result'] == 'Success':
59             # cookies are saved to a file
60             print 'Login successful! (yay)'
61         elif result['login']['result'] == 'NeedToken':
62             print 'Login with token'
63             result = self.api.call({'action': 'login',
64                                     'lgname': user,
65                                     'lgpassword': passwd,
66                                     'lgtoken': result['login']['token']})
67             if result['login']['result'] == 'Success':
68                 print 'Login successful! (yay)'
69             else:
70                 print 'Login failed: %s' % result['login']['result']
71         else:
72             print 'Login failed: %s' % result['login']['result']
73
74     def _die_if_no_init(self):
75         if self.metadir.config is None:
76             print '%s: not a mw repo' % self.me
77             sys.exit(1)
78
79     def _api_setup(self):
80         cookie_file = os.path.join(self.metadir.location, 'cookies')
81         print cookie_file
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
125 class PullCommand(CommandBase):
126
127     def __init__(self):
128         usage = '[options] PAGENAME ...'
129         CommandBase.__init__(self, 'pull', 'add remote pages to repo', usage)
130
131     def _do_command(self):
132         self._die_if_no_init()
133         self._api_setup()
134         pages = []
135         pages += self.args
136         for these_pages in [pages[i:i + 25] for i in range(0, len(pages), 25)]:
137             data = {
138                     'action': 'query',
139                     'titles': '|'.join(these_pages),
140                     'prop': 'info|revisions',
141                     'rvprop': 'ids|flags|timestamp|user|comment|content',
142             }
143             response = self.api.call(data)['query']['pages']
144             for pageid in response.keys():
145                 pagename = response[pageid]['title']
146                 if 'missing' in response[pageid].keys():
147                     print '%s: %s: page does not exist, file not created' % \
148                             (self.me, pagename)
149                     continue
150                 revids = [x['revid'] for x in response[pageid]['revisions']]
151                 revids.sort()
152                 self.metadir.pagedict_add(pagename, pageid, revids[-1])
153                 self.metadir.pages_add_rv(int(pageid),
154                                           response[pageid]['revisions'][0])
155                 filename = mw.metadir.pagename_to_filename(pagename)
156                 with file(os.path.join(self.metadir.root, filename + '.wiki'),
157                           'w') as fd:
158                     data = response[pageid]['revisions'][0]['*']
159                     data = data.encode('utf-8')
160                     fd.write(data)
161
162
163 class StatusCommand(CommandBase):
164
165     def __init__(self):
166         CommandBase.__init__(self, 'status', 'check repo status')
167         self.shortcuts.append('st')
168
169     def _do_command(self):
170         self._die_if_no_init()
171         status = self.metadir.working_dir_status()
172         for file in status:
173             print '%s %s' % (status[file], file)
174
175
176 class DiffCommand(CommandBase):
177
178     def __init__(self):
179         CommandBase.__init__(self, 'diff', 'diff wiki to working directory')
180
181     def _do_command(self):
182         self._die_if_no_init()
183         status = self.metadir.working_dir_status()
184         for file in status:
185             if status[file] == 'U':
186                 print self.metadir.diff_rv_to_working(
187                         mw.metadir.filename_to_pagename(file[:-5])),
188
189
190 class CommitCommand(CommandBase):
191
192     def __init__(self):
193         usage = '[FILES]'
194         CommandBase.__init__(self, 'commit', 'commit changes to wiki', usage)
195         self.shortcuts.append('ci')
196         self.parser.add_option('-m', '--message', dest='edit_summary',
197                                help='don\'t prompt for edit summary and '
198                                'use this instead')
199         self.parser.add_option('-b', '--bot', dest='bot', action='store_true',
200                                help='mark actions as a bot (won\'t affect '
201                                'anything if you don\'t have the bot right',
202                                default=False)
203
204     def _do_command(self):
205         self._die_if_no_init()
206         self._api_setup()
207         status = self.metadir.working_dir_status(files=self.args)
208         nothing_to_commit = True
209         for file in status:
210             print '%s %s' % (status[file], file)
211             if status[file] in ['U']:
212                 nothing_to_commit = False
213         if nothing_to_commit:
214             print 'nothing to commit'
215             sys.exit()
216         if self.options.edit_summary == None:
217             print 'Edit summary:',
218             edit_summary = raw_input()
219         else:
220             edit_summary = self.options.edit_summary
221         for file in status:
222             if status[file] in ['U']:
223                 # get edit token
224                 data = {
225                         'action': 'query',
226                         'prop': 'info|revisions',
227                         'intoken': 'edit',
228                         'titles': mw.metadir.filename_to_pagename(file[:-5]),
229                 }
230                 response = self.api.call(data)
231                 pageid = response['query']['pages'].keys()[0]
232                 revid = response['query']['pages'][pageid]['revisions'][0]\
233                         ['revid']
234                 awaitedrevid = self.metadir.pages_get_rv_list({'id': pageid})\
235                         [0]
236                 if revid != awaitedrevid:
237                     print 'warning: edit conflict detected on %s (%s -> %s) ' \
238                             '-- skipping!' % (file, awaitedrevid, revid)
239                     continue
240                 edittoken = response['query']['pages'][pageid]['edittoken']
241                 filename = os.path.join(self.metadir.root, file)
242                 text = codecs.open(filename, 'r', 'utf-8').read()
243                 text = text.encode('utf-8')
244                 if (len(text) != 0) and (text[-1] == '\n'):
245                     text = text[:-1]
246                 md5 = hashlib.md5()
247                 md5.update(text)
248                 textmd5 = md5.hexdigest()
249                 data = {
250                         'action': 'edit',
251                         'title': mw.metadir.filename_to_pagename(file[:-5]),
252                         'token': edittoken,
253                         'text': text,
254                         'md5': textmd5,
255                         'summary': edit_summary,
256                 }
257                 if self.options.bot:
258                     data['bot'] = 'bot'
259                 response = self.api.call(data)
260                 if response['edit']['result'] == 'Success':
261                     if 'nochange' in response['edit']:
262                         print 'warning: no changes detected in %s - ' \
263                                 'skipping and removing ending LF' % file
264                         self.metadir.clean_page(file[:-5])
265                         continue
266                     if response['edit']['oldrevid'] != revid:
267                         print 'warning: edit conflict detected on %s -- ' \
268                                 'skipping!' % file
269                         continue
270                     data = {
271                             'action': 'query',
272                             'revids': response['edit']['newrevid'],
273                             'prop': 'info|revisions',
274                             'rvprop':
275                                     'ids|flags|timestamp|user|comment|content',
276                     }
277                     response = self.api.call(data)['query']['pages']
278                     self.metadir.pages_add_rv(int(pageid),
279                                               response[pageid]['revisions'][0])
280                 else:
281                     print 'error: committing %s failed: %s' % \
282                             (file, response['edit']['result'])

Benjamin Mako Hill || Want to submit a patch?