A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

683 lines
28 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. from hashlib import md5
  23. import re
  24. from time import gmtime, strftime
  25. from urllib import quote
  26. from earwigbot.wiki.copyright import CopyrightMixin
  27. from earwigbot.wiki.exceptions import *
  28. __all__ = ["Page"]
  29. class Page(CopyrightMixin):
  30. """
  31. EarwigBot's Wiki Toolset: Page Class
  32. Represents a Page on a given Site. Has methods for getting information
  33. about the page, getting page content, and so on. Category is a subclass of
  34. Page with additional methods.
  35. Attributes:
  36. title -- the page's title, or pagename
  37. exists -- whether the page exists
  38. pageid -- an integer ID representing the page
  39. url -- the page's URL
  40. namespace -- the page's namespace as an integer
  41. protection -- the page's current protection status
  42. is_talkpage -- True if the page is a talkpage, else False
  43. is_redirect -- True if the page is a redirect, else False
  44. Public methods:
  45. reload -- forcibly reload the page's attributes
  46. toggle_talk -- returns a content page's talk page, or vice versa
  47. get -- returns page content
  48. get_redirect_target -- if the page is a redirect, returns its destination
  49. get_creator -- returns a User object representing the first person
  50. to edit the page
  51. edit -- replaces the page's content or creates a new page
  52. add_section -- adds a new section at the bottom of the page
  53. copyvio_check -- checks the page for copyright violations
  54. """
  55. re_redirect = "^\s*\#\s*redirect\s*\[\[(.*?)\]\]"
  56. def __init__(self, site, title, follow_redirects=False):
  57. """Constructor for new Page instances.
  58. Takes three arguments: a Site object, the Page's title (or pagename),
  59. and whether or not to follow redirects (optional, defaults to False).
  60. As with User, site.get_page() is preferred. Site's method has support
  61. for a default `follow_redirects` value in our config, while __init__
  62. always defaults to False.
  63. __init__ will not do any API queries, but it will use basic namespace
  64. logic to determine our namespace ID and if we are a talkpage.
  65. """
  66. super(Page, self).__init__(site)
  67. self._site = site
  68. self._title = title.strip()
  69. self._follow_redirects = self._keep_following = follow_redirects
  70. self._exists = 0
  71. self._pageid = None
  72. self._is_redirect = None
  73. self._lastrevid = None
  74. self._protection = None
  75. self._fullurl = None
  76. self._content = None
  77. self._creator = None
  78. # Attributes used for editing/deleting/protecting/etc:
  79. self._token = None
  80. self._basetimestamp = None
  81. self._starttimestamp = None
  82. # Try to determine the page's namespace using our site's namespace
  83. # converter:
  84. prefix = self._title.split(":", 1)[0]
  85. if prefix != title: # ignore a page that's titled "Category" or "User"
  86. try:
  87. self._namespace = self._site.namespace_name_to_id(prefix)
  88. except NamespaceNotFoundError:
  89. self._namespace = 0
  90. else:
  91. self._namespace = 0
  92. # Is this a talkpage? Talkpages have odd IDs, while content pages have
  93. # even IDs, excluding the "special" namespaces:
  94. if self._namespace < 0:
  95. self._is_talkpage = False
  96. else:
  97. self._is_talkpage = self._namespace % 2 == 1
  98. def __repr__(self):
  99. """Returns the canonical string representation of the Page."""
  100. res = "Page(title={0!r}, follow_redirects={1!r}, site={2!r})"
  101. return res.format(self._title, self._follow_redirects, self._site)
  102. def __str__(self):
  103. """Returns a nice string representation of the Page."""
  104. return '<Page "{0}" of {1}>'.format(self.title(), str(self._site))
  105. def _force_validity(self):
  106. """Used to ensure that our page's title is valid.
  107. If this method is called when our page is not valid (and after
  108. _load_attributes() has been called), InvalidPageError will be raised.
  109. Note that validity != existence. If a page's title is invalid (e.g, it
  110. contains "[") it will always be invalid, and cannot be edited.
  111. """
  112. if self._exists == 1:
  113. e = "Page '{0}' is invalid.".format(self._title)
  114. raise InvalidPageError(e)
  115. def _force_existence(self):
  116. """Used to ensure that our page exists.
  117. If this method is called when our page doesn't exist (and after
  118. _load_attributes() has been called), PageNotFoundError will be raised.
  119. It will also call _force_validity() beforehand.
  120. """
  121. self._force_validity()
  122. if self._exists == 2:
  123. e = "Page '{0}' does not exist.".format(self._title)
  124. raise PageNotFoundError(e)
  125. def _load_wrapper(self):
  126. """Calls _load_attributes() and follows redirects if we're supposed to.
  127. This method will only follow redirects if follow_redirects=True was
  128. passed to __init__() (perhaps indirectly passed by site.get_page()).
  129. It avoids the API's &redirects param in favor of manual following,
  130. so we can act more realistically (we don't follow double redirects, and
  131. circular redirects don't break us).
  132. This will raise RedirectError if we have a problem following, but that
  133. is a bug and should NOT happen.
  134. If we're following a redirect, this will make a grand total of three
  135. API queries. It's a lot, but each one is quite small.
  136. """
  137. self._load_attributes()
  138. if self._keep_following and self._is_redirect:
  139. self._title = self.get_redirect_target()
  140. self._keep_following = False # don't follow double redirects
  141. self._content = None # reset the content we just loaded
  142. self._load_attributes()
  143. def _load_attributes(self, result=None):
  144. """Loads various data from the API in a single query.
  145. Loads self._title, ._exists, ._is_redirect, ._pageid, ._fullurl,
  146. ._protection, ._namespace, ._is_talkpage, ._creator, ._lastrevid,
  147. ._token, and ._starttimestamp using the API. It will do a query of
  148. its own unless `result` is provided, in which case we'll pretend
  149. `result` is what the query returned.
  150. Assuming the API is sound, this should not raise any exceptions.
  151. """
  152. if not result:
  153. params = {"action": "query", "rvprop": "user", "intoken": "edit",
  154. "prop": "info|revisions", "rvlimit": 1, "rvdir": "newer",
  155. "titles": self._title, "inprop": "protection|url"}
  156. result = self._site._api_query(params)
  157. res = result["query"]["pages"].values()[0]
  158. # Normalize our pagename/title thing:
  159. self._title = res["title"]
  160. try:
  161. res["redirect"]
  162. except KeyError:
  163. self._is_redirect = False
  164. else:
  165. self._is_redirect = True
  166. self._pageid = int(result["query"]["pages"].keys()[0])
  167. if self._pageid < 0:
  168. if "missing" in res:
  169. # If it has a negative ID and it's missing; we can still get
  170. # data like the namespace, protection, and URL:
  171. self._exists = 2
  172. else:
  173. # If it has a negative ID and it's invalid, then break here,
  174. # because there's no other data for us to get:
  175. self._exists = 1
  176. return
  177. else:
  178. self._exists = 3
  179. self._fullurl = res["fullurl"]
  180. self._protection = res["protection"]
  181. try:
  182. self._token = res["edittoken"]
  183. except KeyError:
  184. pass
  185. else:
  186. self._starttimestamp = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime())
  187. # We've determined the namespace and talkpage status in __init__()
  188. # based on the title, but now we can be sure:
  189. self._namespace = res["ns"]
  190. self._is_talkpage = self._namespace % 2 == 1 # talkpages have odd IDs
  191. # These last two fields will only be specified if the page exists:
  192. self._lastrevid = res.get("lastrevid")
  193. try:
  194. self._creator = res['revisions'][0]['user']
  195. except KeyError:
  196. pass
  197. def _load_content(self, result=None):
  198. """Loads current page content from the API.
  199. If `result` is provided, we'll pretend that is the result of an API
  200. query and try to get content from that. Otherwise, we'll do an API
  201. query on our own.
  202. Don't call this directly, ever - use .get(force=True) if you want to
  203. force content reloading.
  204. """
  205. if not result:
  206. params = {"action": "query", "prop": "revisions", "rvlimit": 1,
  207. "rvprop": "content|timestamp", "titles": self._title}
  208. result = self._site._api_query(params)
  209. res = result["query"]["pages"].values()[0]
  210. try:
  211. self._content = res["revisions"][0]["*"]
  212. self._basetimestamp = res["revisions"][0]["timestamp"]
  213. except KeyError:
  214. # This can only happen if the page was deleted since we last called
  215. # self._load_attributes(). In that case, some of our attributes are
  216. # outdated, so force another self._load_attributes():
  217. self._load_attributes()
  218. self._force_existence()
  219. def _edit(self, params=None, text=None, summary=None, minor=None, bot=None,
  220. force=None, section=None, captcha_id=None, captcha_word=None,
  221. tries=0):
  222. """Edit the page!
  223. If `params` is given, we'll use it as our API query parameters.
  224. Otherwise, we'll build params using the given kwargs via
  225. _build_edit_params().
  226. We'll then try to do the API query, and catch any errors the API raises
  227. in _handle_edit_errors(). We'll then throw these back as subclasses of
  228. EditError.
  229. """
  230. # Try to get our edit token, and die if we can't:
  231. if not self._token:
  232. self._load_attributes()
  233. if not self._token:
  234. e = "You don't have permission to edit this page."
  235. raise PermissionsError(e)
  236. # Weed out invalid pages before we get too far:
  237. self._force_validity()
  238. # Build our API query string:
  239. if not params:
  240. params = self._build_edit_params(text, summary, minor, bot, force,
  241. section, captcha_id, captcha_word)
  242. else: # Make sure we have the right token:
  243. params["token"] = self._token
  244. # Try the API query, catching most errors with our handler:
  245. try:
  246. result = self._site._api_query(params)
  247. except SiteAPIError as error:
  248. if not hasattr(error, "code"):
  249. raise # We can only handle errors with a code attribute
  250. result = self._handle_edit_errors(error, params, tries)
  251. # If everything was successful, reset invalidated attributes:
  252. if result["edit"]["result"] == "Success":
  253. self._content = None
  254. self._basetimestamp = None
  255. self._exists = 0
  256. return
  257. # If we're here, then the edit failed. If it's because of AssertEdit,
  258. # handle that. Otherwise, die - something odd is going on:
  259. try:
  260. assertion = result["edit"]["assert"]
  261. except KeyError:
  262. raise EditError(result["edit"])
  263. self._handle_assert_edit(assertion, params, tries)
  264. def _build_edit_params(self, text, summary, minor, bot, force, section,
  265. captcha_id, captcha_word):
  266. """Given some keyword arguments, build an API edit query string."""
  267. hashed = md5(text).hexdigest() # Checksum to ensure text is correct
  268. params = {"action": "edit", "title": self._title, "text": text,
  269. "token": self._token, "summary": summary, "md5": hashed}
  270. if section:
  271. params["section"] = section
  272. if captcha_id and captcha_word:
  273. params["captchaid"] = captcha_id
  274. params["captchaword"] = captcha_word
  275. if minor:
  276. params["minor"] = "true"
  277. else:
  278. params["notminor"] = "true"
  279. if bot:
  280. params["bot"] = "true"
  281. if not force:
  282. params["starttimestamp"] = self._starttimestamp
  283. if self._basetimestamp:
  284. params["basetimestamp"] = self._basetimestamp
  285. if self._exists == 2:
  286. # Page does not exist; don't edit if it already exists:
  287. params["createonly"] = "true"
  288. else:
  289. params["recreate"] = "true"
  290. return params
  291. def _handle_edit_errors(self, error, params, tries):
  292. """If our edit fails due to some error, try to handle it.
  293. We'll either raise an appropriate exception (for example, if the page
  294. is protected), or we'll try to fix it (for example, if we can't edit
  295. due to being logged out, we'll try to log in).
  296. """
  297. if error.code in ["noedit", "cantcreate", "protectedtitle",
  298. "noimageredirect"]:
  299. raise PermissionsError(error.info)
  300. elif error.code in ["noedit-anon", "cantcreate-anon",
  301. "noimageredirect-anon"]:
  302. if not all(self._site._login_info):
  303. # Insufficient login info:
  304. raise PermissionsError(error.info)
  305. if tries == 0:
  306. # We have login info; try to login:
  307. self._site._login(self._site._login_info)
  308. self._token = None # Need a new token; old one is invalid now
  309. return self._edit(params=params, tries=1)
  310. else:
  311. # We already tried to log in and failed!
  312. e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug."
  313. raise LoginError(e)
  314. elif error.code in ["editconflict", "pagedeleted", "articleexists"]:
  315. # These attributes are now invalidated:
  316. self._content = None
  317. self._basetimestamp = None
  318. self._exists = 0
  319. raise EditConflictError(error.info)
  320. elif error.code in ["emptypage", "emptynewsection"]:
  321. raise NoContentError(error.info)
  322. elif error.code == "contenttoobig":
  323. raise ContentTooBigError(error.info)
  324. elif error.code == "spamdetected":
  325. raise SpamDetectedError(error.info)
  326. elif error.code == "filtered":
  327. raise FilteredError(error.info)
  328. raise EditError(": ".join((error.code, error.info)))
  329. def _handle_assert_edit(self, assertion, params, tries):
  330. """If we can't edit due to a failed AssertEdit assertion, handle that.
  331. If the assertion was 'user' and we have valid login information, try to
  332. log in. Otherwise, raise PermissionsError with details.
  333. """
  334. if assertion == "user":
  335. if not all(self._site._login_info):
  336. # Insufficient login info:
  337. e = "AssertEdit: user assertion failed, and no login info was provided."
  338. raise PermissionsError(e)
  339. if tries == 0:
  340. # We have login info; try to login:
  341. self._site._login(self._site._login_info)
  342. self._token = None # Need a new token; old one is invalid now
  343. return self._edit(params=params, tries=1)
  344. else:
  345. # We already tried to log in and failed!
  346. e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug."
  347. raise LoginError(e)
  348. elif assertion == "bot":
  349. e = "AssertEdit: bot assertion failed; we don't have a bot flag!"
  350. raise PermissionsError(e)
  351. # Unknown assertion, maybe "true", "false", or "exists":
  352. e = "AssertEdit: assertion '{0}' failed.".format(assertion)
  353. raise PermissionsError(e)
  354. def title(self, force=False):
  355. """Returns the Page's title, or pagename.
  356. This won't do any API queries on its own unless force is True, in which
  357. case the title will be forcibly reloaded from the API (normalizing it,
  358. and following redirects if follow_redirects=True was passed to
  359. __init__()). Any other methods that do API queries will reload title on
  360. their own, however, like exists() and get().
  361. """
  362. if force:
  363. self._load_wrapper()
  364. return self._title
  365. def exists(self, force=False):
  366. """Returns information about whether the Page exists or not.
  367. The returned "information" is a tuple with two items. The first is a
  368. bool, either True if the page exists or False if it does not. The
  369. second is a string giving more information, either "invalid", (title
  370. is invalid, e.g. it contains "["), "missing", or "exists".
  371. Makes an API query if force is True or if we haven't already made one.
  372. """
  373. cases = {
  374. 0: (None, "unknown"),
  375. 1: (False, "invalid"),
  376. 2: (False, "missing"),
  377. 3: (True, "exists"),
  378. }
  379. if self._exists == 0 or force:
  380. self._load_wrapper()
  381. return cases[self._exists]
  382. def pageid(self, force=False):
  383. """Returns an integer ID representing the Page.
  384. Makes an API query if force is True or if we haven't already made one.
  385. Raises InvalidPageError or PageNotFoundError if the page name is
  386. invalid or the page does not exist, respectively.
  387. """
  388. if self._exists == 0 or force:
  389. self._load_wrapper()
  390. self._force_existence() # missing pages do not have IDs
  391. return self._pageid
  392. def url(self, force=False):
  393. """Returns the page's URL.
  394. Like title(), this won't do any API queries on its own unless force is
  395. True. If the API was never queried for this page, we will attempt to
  396. determine the URL ourselves based on the title.
  397. """
  398. if force:
  399. self._load_wrapper()
  400. if self._fullurl:
  401. return self._fullurl
  402. else:
  403. slug = quote(self._title.replace(" ", "_"), safe="/:")
  404. path = self._site._article_path.replace("$1", slug)
  405. return ''.join((self._site._base_url, path))
  406. def namespace(self, force=False):
  407. """Returns the page's namespace ID (an integer).
  408. Like title(), this won't do any API queries on its own unless force is
  409. True. If the API was never queried for this page, we will attempt to
  410. determine the namespace ourselves based on the title.
  411. """
  412. if force:
  413. self._load_wrapper()
  414. return self._namespace
  415. def protection(self, force=False):
  416. """Returns the page's current protection status.
  417. Makes an API query if force is True or if we haven't already made one.
  418. Raises InvalidPageError if the page name is invalid. Will not raise an
  419. error if the page is missing because those can still be protected.
  420. """
  421. if self._exists == 0 or force:
  422. self._load_wrapper()
  423. self._force_validity() # invalid pages cannot be protected
  424. return self._protection
  425. def creator(self, force=False):
  426. """Returns the page's creator (i.e., the first user to edit the page).
  427. Makes an API query if force is True or if we haven't already made one.
  428. Normally, we can get the creator along with everything else (except
  429. content) in self._load_attributes(). However, due to a limitation in
  430. the API (can't get the editor of one revision and the content of
  431. another at both ends of the history), if our other attributes were only
  432. loaded from get(), we'll have to do another API query. This is done
  433. by calling ourselves again with force=True.
  434. Raises InvalidPageError or PageNotFoundError if the page name is
  435. invalid or the page does not exist, respectively.
  436. """
  437. if self._exists == 0 or force:
  438. self._load_wrapper()
  439. self._force_existence()
  440. if not self._creator and not force:
  441. self.creator(force=True)
  442. return self._creator
  443. def is_talkpage(self, force=False):
  444. """Returns True if the page is a talkpage, else False.
  445. Like title(), this won't do any API queries on its own unless force is
  446. True. If the API was never queried for this page, we will attempt to
  447. determine the talkpage status ourselves based on its namespace ID.
  448. """
  449. if force:
  450. self._load_wrapper()
  451. return self._is_talkpage
  452. def is_redirect(self, force=False):
  453. """Returns True if the page is a redirect, else False.
  454. Makes an API query if force is True or if we haven't already made one.
  455. We will return False even if the page does not exist or is invalid.
  456. """
  457. if self._exists == 0 or force:
  458. self._load_wrapper()
  459. return self._is_redirect
  460. def toggle_talk(self, force=False, follow_redirects=None):
  461. """Returns a content page's talk page, or vice versa.
  462. The title of the new page is determined by namespace logic, not API
  463. queries. We won't make any API queries on our own unless force is True,
  464. and the only reason then would be to forcibly update the title or
  465. follow redirects if we haven't already made an API query.
  466. If `follow_redirects` is anything other than None (the default), it
  467. will be passed to the new Page's __init__(). Otherwise, we'll use the
  468. value passed to our own __init__().
  469. Will raise InvalidPageError if we try to get the talk page of a special
  470. page (in the Special: or Media: namespaces), but we won't raise an
  471. exception if our page is otherwise missing or invalid.
  472. """
  473. if force:
  474. self._load_wrapper()
  475. if self._namespace < 0:
  476. ns = self._site.namespace_id_to_name(self._namespace)
  477. e = "Pages in the {0} namespace can't have talk pages.".format(ns)
  478. raise InvalidPageError(e)
  479. if self._is_talkpage:
  480. new_ns = self._namespace - 1
  481. else:
  482. new_ns = self._namespace + 1
  483. try:
  484. body = self._title.split(":", 1)[1]
  485. except IndexError:
  486. body = self._title
  487. new_prefix = self._site.namespace_id_to_name(new_ns)
  488. # If the new page is in namespace 0, don't do ":Title" (it's correct,
  489. # but unnecessary), just do "Title":
  490. if new_prefix:
  491. new_title = ':'.join((new_prefix, body))
  492. else:
  493. new_title = body
  494. if follow_redirects is None:
  495. follow_redirects = self._follow_redirects
  496. return Page(self._site, new_title, follow_redirects)
  497. def get(self, force=False):
  498. """Returns page content, which is cached if you try to call get again.
  499. Use `force` to forcibly reload page content even if we've already
  500. loaded some. This is good if you want to edit a page multiple times,
  501. and you want to get updated content before you make your second edit.
  502. Raises InvalidPageError or PageNotFoundError if the page name is
  503. invalid or the page does not exist, respectively.
  504. """
  505. if force or self._exists == 0:
  506. # Kill two birds with one stone by doing an API query for both our
  507. # attributes and our page content:
  508. params = {"action": "query", "rvlimit": 1, "titles": self._title,
  509. "prop": "info|revisions", "inprop": "protection|url",
  510. "intoken": "edit", "rvprop": "content|timestamp"}
  511. result = self._site._api_query(params)
  512. self._load_attributes(result=result)
  513. self._force_existence()
  514. self._load_content(result=result)
  515. # Follow redirects if we're told to:
  516. if self._keep_following and self._is_redirect:
  517. self._title = self.get_redirect_target()
  518. self._keep_following = False # don't follow double redirects
  519. self._content = None # reset the content we just loaded
  520. self.get(force=True)
  521. return self._content
  522. # Make sure we're dealing with a real page here. This may be outdated
  523. # if the page was deleted since we last called self._load_attributes(),
  524. # but self._load_content() can handle that:
  525. self._force_existence()
  526. if self._content is None:
  527. self._load_content()
  528. return self._content
  529. def get_redirect_target(self, force=False):
  530. """If the page is a redirect, returns its destination.
  531. Use `force` to forcibly reload content even if we've already loaded
  532. some before. Note that this method calls get() for page content.
  533. Raises InvalidPageError or PageNotFoundError if the page name is
  534. invalid or the page does not exist, respectively. Raises RedirectError
  535. if the page is not a redirect.
  536. """
  537. content = self.get(force)
  538. try:
  539. return re.findall(self.re_redirect, content, flags=re.I)[0]
  540. except IndexError:
  541. e = "The page does not appear to have a redirect target."
  542. raise RedirectError(e)
  543. def edit(self, text, summary, minor=False, bot=True, force=False):
  544. """Replaces the page's content or creates a new page.
  545. `text` is the new page content, with `summary` as the edit summary.
  546. If `minor` is True, the edit will be marked as minor. If `bot` is true,
  547. the edit will be marked as a bot edit, but only if we actually have a
  548. bot flag.
  549. Use `force` to push the new content even if there's an edit conflict or
  550. the page was deleted/recreated between getting our edit token and
  551. editing our page. Be careful with this!
  552. """
  553. self._edit(text=text, summary=summary, minor=minor, bot=bot,
  554. force=force)
  555. def add_section(self, text, title, minor=False, bot=True, force=False):
  556. """Adds a new section to the bottom of the page.
  557. The arguments for this are the same as those for edit(), but instead of
  558. providing a summary, you provide a section title.
  559. Likewise, raised exceptions are the same as edit()'s.
  560. This should create the page if it does not already exist, with just the
  561. new section as content.
  562. """
  563. self._edit(text=text, summary=title, minor=minor, bot=bot, force=force,
  564. section="new")