@@ -0,0 +1,7 @@ | |||||
repos: | |||||
- repo: https://github.com/astral-sh/ruff-pre-commit | |||||
rev: v0.3.5 | |||||
hooks: | |||||
- id: ruff | |||||
args: [--fix] | |||||
- id: ruff-format |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# EarwigBot documentation build configuration file, created by | # EarwigBot documentation build configuration file, created by | ||||
# sphinx-quickstart on Sun Apr 29 01:42:25 2012. | # sphinx-quickstart on Sun Apr 29 01:42:25 2012. | ||||
# | # | ||||
@@ -11,214 +9,209 @@ | |||||
# All configuration values have a default; values that are commented out | # All configuration values have a default; values that are commented out | ||||
# serve to show the default. | # serve to show the default. | ||||
import sys, os | |||||
import os | |||||
import sys | |||||
# If extensions (or modules to document with autodoc) are in another directory, | # If extensions (or modules to document with autodoc) are in another directory, | ||||
# add these directories to sys.path here. If the directory is relative to the | # add these directories to sys.path here. If the directory is relative to the | ||||
# documentation root, use os.path.abspath to make it absolute, like shown here. | # documentation root, use os.path.abspath to make it absolute, like shown here. | ||||
sys.path.insert(0, os.path.abspath('..')) | |||||
sys.path.insert(0, os.path.abspath("..")) | |||||
# -- General configuration ----------------------------------------------------- | # -- General configuration ----------------------------------------------------- | ||||
# If your documentation needs a minimal Sphinx version, state it here. | # If your documentation needs a minimal Sphinx version, state it here. | ||||
#needs_sphinx = '1.0' | |||||
# needs_sphinx = '1.0' | |||||
# Add any Sphinx extension module names here, as strings. They can be extensions | # Add any Sphinx extension module names here, as strings. They can be extensions | ||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. | ||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] | |||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] | |||||
# Add any paths that contain templates here, relative to this directory. | # Add any paths that contain templates here, relative to this directory. | ||||
templates_path = ['_templates'] | |||||
templates_path = ["_templates"] | |||||
# The suffix of source filenames. | # The suffix of source filenames. | ||||
source_suffix = '.rst' | |||||
source_suffix = ".rst" | |||||
# The encoding of source files. | # The encoding of source files. | ||||
#source_encoding = 'utf-8-sig' | |||||
# source_encoding = 'utf-8-sig' | |||||
# The master toctree document. | # The master toctree document. | ||||
master_doc = 'index' | |||||
master_doc = "index" | |||||
# General information about the project. | # General information about the project. | ||||
project = u'EarwigBot' | |||||
copyright = u'2009-2016 Ben Kurtovic' | |||||
project = "EarwigBot" | |||||
copyright = "2009-2016 Ben Kurtovic" | |||||
# The version info for the project you're documenting, acts as replacement for | # The version info for the project you're documenting, acts as replacement for | ||||
# |version| and |release|, also used in various other places throughout the | # |version| and |release|, also used in various other places throughout the | ||||
# built documents. | # built documents. | ||||
# | # | ||||
# The short X.Y version. | # The short X.Y version. | ||||
version = '0.4' | |||||
version = "0.4" | |||||
# The full version, including alpha/beta/rc tags. | # The full version, including alpha/beta/rc tags. | ||||
release = '0.4.dev0' | |||||
release = "0.4.dev0" | |||||
# The language for content autogenerated by Sphinx. Refer to documentation | # The language for content autogenerated by Sphinx. Refer to documentation | ||||
# for a list of supported languages. | # for a list of supported languages. | ||||
#language = None | |||||
# language = None | |||||
# There are two options for replacing |today|: either, you set today to some | # There are two options for replacing |today|: either, you set today to some | ||||
# non-false value, then it is used: | # non-false value, then it is used: | ||||
#today = '' | |||||
# today = '' | |||||
# Else, today_fmt is used as the format for a strftime call. | # Else, today_fmt is used as the format for a strftime call. | ||||
#today_fmt = '%B %d, %Y' | |||||
# today_fmt = '%B %d, %Y' | |||||
# List of patterns, relative to source directory, that match files and | # List of patterns, relative to source directory, that match files and | ||||
# directories to ignore when looking for source files. | # directories to ignore when looking for source files. | ||||
exclude_patterns = ['_build'] | |||||
exclude_patterns = ["_build"] | |||||
# The reST default role (used for this markup: `text`) to use for all documents. | # The reST default role (used for this markup: `text`) to use for all documents. | ||||
#default_role = None | |||||
# default_role = None | |||||
# If true, '()' will be appended to :func: etc. cross-reference text. | # If true, '()' will be appended to :func: etc. cross-reference text. | ||||
#add_function_parentheses = True | |||||
# add_function_parentheses = True | |||||
# If true, the current module name will be prepended to all description | # If true, the current module name will be prepended to all description | ||||
# unit titles (such as .. function::). | # unit titles (such as .. function::). | ||||
#add_module_names = True | |||||
# add_module_names = True | |||||
# If true, sectionauthor and moduleauthor directives will be shown in the | # If true, sectionauthor and moduleauthor directives will be shown in the | ||||
# output. They are ignored by default. | # output. They are ignored by default. | ||||
#show_authors = False | |||||
# show_authors = False | |||||
# The name of the Pygments (syntax highlighting) style to use. | # The name of the Pygments (syntax highlighting) style to use. | ||||
pygments_style = 'sphinx' | |||||
pygments_style = "sphinx" | |||||
# A list of ignored prefixes for module index sorting. | # A list of ignored prefixes for module index sorting. | ||||
#modindex_common_prefix = [] | |||||
# modindex_common_prefix = [] | |||||
# -- Options for HTML output --------------------------------------------------- | # -- Options for HTML output --------------------------------------------------- | ||||
# The theme to use for HTML and HTML Help pages. See the documentation for | # The theme to use for HTML and HTML Help pages. See the documentation for | ||||
# a list of builtin themes. | # a list of builtin themes. | ||||
html_theme = 'nature' | |||||
html_theme = "nature" | |||||
# Theme options are theme-specific and customize the look and feel of a theme | # Theme options are theme-specific and customize the look and feel of a theme | ||||
# further. For a list of options available for each theme, see the | # further. For a list of options available for each theme, see the | ||||
# documentation. | # documentation. | ||||
#html_theme_options = {} | |||||
# html_theme_options = {} | |||||
# Add any paths that contain custom themes here, relative to this directory. | # Add any paths that contain custom themes here, relative to this directory. | ||||
#html_theme_path = [] | |||||
# html_theme_path = [] | |||||
# The name for this set of Sphinx documents. If None, it defaults to | # The name for this set of Sphinx documents. If None, it defaults to | ||||
# "<project> v<release> documentation". | # "<project> v<release> documentation". | ||||
#html_title = None | |||||
# html_title = None | |||||
# A shorter title for the navigation bar. Default is the same as html_title. | # A shorter title for the navigation bar. Default is the same as html_title. | ||||
#html_short_title = None | |||||
# html_short_title = None | |||||
# The name of an image file (relative to this directory) to place at the top | # The name of an image file (relative to this directory) to place at the top | ||||
# of the sidebar. | # of the sidebar. | ||||
#html_logo = None | |||||
# html_logo = None | |||||
# The name of an image file (within the static path) to use as favicon of the | # The name of an image file (within the static path) to use as favicon of the | ||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 | ||||
# pixels large. | # pixels large. | ||||
#html_favicon = None | |||||
# html_favicon = None | |||||
# Add any paths that contain custom static files (such as style sheets) here, | # Add any paths that contain custom static files (such as style sheets) here, | ||||
# relative to this directory. They are copied after the builtin static files, | # relative to this directory. They are copied after the builtin static files, | ||||
# so a file named "default.css" will overwrite the builtin "default.css". | # so a file named "default.css" will overwrite the builtin "default.css". | ||||
html_static_path = ['_static'] | |||||
html_static_path = ["_static"] | |||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, | ||||
# using the given strftime format. | # using the given strftime format. | ||||
#html_last_updated_fmt = '%b %d, %Y' | |||||
# html_last_updated_fmt = '%b %d, %Y' | |||||
# If true, SmartyPants will be used to convert quotes and dashes to | # If true, SmartyPants will be used to convert quotes and dashes to | ||||
# typographically correct entities. | # typographically correct entities. | ||||
#html_use_smartypants = True | |||||
# html_use_smartypants = True | |||||
# Custom sidebar templates, maps document names to template names. | # Custom sidebar templates, maps document names to template names. | ||||
#html_sidebars = {} | |||||
# html_sidebars = {} | |||||
# Additional templates that should be rendered to pages, maps page names to | # Additional templates that should be rendered to pages, maps page names to | ||||
# template names. | # template names. | ||||
#html_additional_pages = {} | |||||
# html_additional_pages = {} | |||||
# If false, no module index is generated. | # If false, no module index is generated. | ||||
#html_domain_indices = True | |||||
# html_domain_indices = True | |||||
# If false, no index is generated. | # If false, no index is generated. | ||||
#html_use_index = True | |||||
# html_use_index = True | |||||
# If true, the index is split into individual pages for each letter. | # If true, the index is split into individual pages for each letter. | ||||
#html_split_index = False | |||||
# html_split_index = False | |||||
# If true, links to the reST sources are added to the pages. | # If true, links to the reST sources are added to the pages. | ||||
#html_show_sourcelink = True | |||||
# html_show_sourcelink = True | |||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | ||||
#html_show_sphinx = True | |||||
# html_show_sphinx = True | |||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | ||||
#html_show_copyright = True | |||||
# html_show_copyright = True | |||||
# If true, an OpenSearch description file will be output, and all pages will | # If true, an OpenSearch description file will be output, and all pages will | ||||
# contain a <link> tag referring to it. The value of this option must be the | # contain a <link> tag referring to it. The value of this option must be the | ||||
# base URL from which the finished HTML is served. | # base URL from which the finished HTML is served. | ||||
#html_use_opensearch = '' | |||||
# html_use_opensearch = '' | |||||
# This is the file name suffix for HTML files (e.g. ".xhtml"). | # This is the file name suffix for HTML files (e.g. ".xhtml"). | ||||
#html_file_suffix = None | |||||
# html_file_suffix = None | |||||
# Output file base name for HTML help builder. | # Output file base name for HTML help builder. | ||||
htmlhelp_basename = 'EarwigBotdoc' | |||||
htmlhelp_basename = "EarwigBotdoc" | |||||
# -- Options for LaTeX output -------------------------------------------------- | # -- Options for LaTeX output -------------------------------------------------- | ||||
latex_elements = { | latex_elements = { | ||||
# The paper size ('letterpaper' or 'a4paper'). | |||||
#'papersize': 'letterpaper', | |||||
# The font size ('10pt', '11pt' or '12pt'). | |||||
#'pointsize': '10pt', | |||||
# Additional stuff for the LaTeX preamble. | |||||
#'preamble': '', | |||||
# The paper size ('letterpaper' or 'a4paper'). | |||||
#'papersize': 'letterpaper', | |||||
# The font size ('10pt', '11pt' or '12pt'). | |||||
#'pointsize': '10pt', | |||||
# Additional stuff for the LaTeX preamble. | |||||
#'preamble': '', | |||||
} | } | ||||
# Grouping the document tree into LaTeX files. List of tuples | # Grouping the document tree into LaTeX files. List of tuples | ||||
# (source start file, target name, title, author, documentclass [howto/manual]). | # (source start file, target name, title, author, documentclass [howto/manual]). | ||||
latex_documents = [ | latex_documents = [ | ||||
('index', 'EarwigBot.tex', u'EarwigBot Documentation', | |||||
u'Ben Kurtovic', 'manual'), | |||||
("index", "EarwigBot.tex", "EarwigBot Documentation", "Ben Kurtovic", "manual"), | |||||
] | ] | ||||
# The name of an image file (relative to this directory) to place at the top of | # The name of an image file (relative to this directory) to place at the top of | ||||
# the title page. | # the title page. | ||||
#latex_logo = None | |||||
# latex_logo = None | |||||
# For "manual" documents, if this is true, then toplevel headings are parts, | # For "manual" documents, if this is true, then toplevel headings are parts, | ||||
# not chapters. | # not chapters. | ||||
#latex_use_parts = False | |||||
# latex_use_parts = False | |||||
# If true, show page references after internal links. | # If true, show page references after internal links. | ||||
#latex_show_pagerefs = False | |||||
# latex_show_pagerefs = False | |||||
# If true, show URL addresses after external links. | # If true, show URL addresses after external links. | ||||
#latex_show_urls = False | |||||
# latex_show_urls = False | |||||
# Documents to append as an appendix to all manuals. | # Documents to append as an appendix to all manuals. | ||||
#latex_appendices = [] | |||||
# latex_appendices = [] | |||||
# If false, no module index is generated. | # If false, no module index is generated. | ||||
#latex_domain_indices = True | |||||
# latex_domain_indices = True | |||||
# -- Options for manual page output -------------------------------------------- | # -- Options for manual page output -------------------------------------------- | ||||
# One entry per manual page. List of tuples | # One entry per manual page. List of tuples | ||||
# (source start file, name, description, authors, manual section). | # (source start file, name, description, authors, manual section). | ||||
man_pages = [ | |||||
('index', 'earwigbot', u'EarwigBot Documentation', | |||||
[u'Ben Kurtovic'], 1) | |||||
] | |||||
man_pages = [("index", "earwigbot", "EarwigBot Documentation", ["Ben Kurtovic"], 1)] | |||||
# If true, show URL addresses after external links. | # If true, show URL addresses after external links. | ||||
#man_show_urls = False | |||||
# man_show_urls = False | |||||
# -- Options for Texinfo output ------------------------------------------------ | # -- Options for Texinfo output ------------------------------------------------ | ||||
@@ -227,16 +220,22 @@ man_pages = [ | |||||
# (source start file, target name, title, author, | # (source start file, target name, title, author, | ||||
# dir menu entry, description, category) | # dir menu entry, description, category) | ||||
texinfo_documents = [ | texinfo_documents = [ | ||||
('index', 'EarwigBot', u'EarwigBot Documentation', | |||||
u'Ben Kurtovic', 'EarwigBot', 'One line description of project.', | |||||
'Miscellaneous'), | |||||
( | |||||
"index", | |||||
"EarwigBot", | |||||
"EarwigBot Documentation", | |||||
"Ben Kurtovic", | |||||
"EarwigBot", | |||||
"EarwigBot is a Python robot that edits Wikipedia and interacts with people over IRC.", | |||||
"Miscellaneous", | |||||
), | |||||
] | ] | ||||
# Documents to append as an appendix to all manuals. | # Documents to append as an appendix to all manuals. | ||||
#texinfo_appendices = [] | |||||
# texinfo_appendices = [] | |||||
# If false, no module index is generated. | # If false, no module index is generated. | ||||
#texinfo_domain_indices = True | |||||
# texinfo_domain_indices = True | |||||
# How to display URL addresses: 'footnote', 'no', or 'inline'. | # How to display URL addresses: 'footnote', 'no', or 'inline'. | ||||
#texinfo_show_urls = 'footnote' | |||||
# texinfo_show_urls = 'footnote' |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -37,13 +35,17 @@ __email__ = "ben.kurtovic@gmail.com" | |||||
__release__ = False | __release__ = False | ||||
if not __release__: | if not __release__: | ||||
def _get_git_commit_id(): | def _get_git_commit_id(): | ||||
"""Return the ID of the git HEAD commit.""" | """Return the ID of the git HEAD commit.""" | ||||
from os.path import dirname, split | |||||
from git import Repo | from git import Repo | ||||
from os.path import split, dirname | |||||
path = split(dirname(__file__))[0] | path = split(dirname(__file__))[0] | ||||
commit_id = Repo(path).head.object.hexsha | commit_id = Repo(path).head.object.hexsha | ||||
return commit_id[:8] | return commit_id[:8] | ||||
try: | try: | ||||
__version__ += "+" + _get_git_commit_id() | __version__ += "+" + _get_git_commit_id() | ||||
except Exception: | except Exception: | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,7 +19,8 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import logging | import logging | ||||
from threading import Lock, Thread, enumerate as enumerate_threads | |||||
from threading import Lock, Thread | |||||
from threading import enumerate as enumerate_threads | |||||
from time import gmtime, sleep | from time import gmtime, sleep | ||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
@@ -32,6 +31,7 @@ from earwigbot.wiki import SitesDB | |||||
__all__ = ["Bot"] | __all__ = ["Bot"] | ||||
class Bot: | class Bot: | ||||
""" | """ | ||||
**EarwigBot: Main Bot Class** | **EarwigBot: Main Bot Class** | ||||
@@ -77,11 +77,11 @@ class Bot: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the Bot.""" | """Return the canonical string representation of the Bot.""" | ||||
return "Bot(config={0!r})".format(self.config) | |||||
return f"Bot(config={self.config!r})" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Bot.""" | """Return a nice string representation of the Bot.""" | ||||
return "<Bot at {0}>".format(self.config.root_dir) | |||||
return f"<Bot at {self.config.root_dir}>" | |||||
def _dispatch_irc_component(self, name, klass): | def _dispatch_irc_component(self, name, klass): | ||||
"""Create a new IRC component, record it internally, and start it.""" | """Create a new IRC component, record it internally, and start it.""" | ||||
@@ -100,6 +100,7 @@ class Bot: | |||||
def _start_wiki_scheduler(self): | def _start_wiki_scheduler(self): | ||||
"""Start the wiki scheduler in a separate thread if enabled.""" | """Start the wiki scheduler in a separate thread if enabled.""" | ||||
def wiki_scheduler(): | def wiki_scheduler(): | ||||
run_at = 15 | run_at = 15 | ||||
while self._keep_looping: | while self._keep_looping: | ||||
@@ -118,7 +119,7 @@ class Bot: | |||||
if component: | if component: | ||||
component.keep_alive() | component.keep_alive() | ||||
if component.is_stopped(): | if component.is_stopped(): | ||||
log = "IRC {0} has stopped; restarting".format(name) | |||||
log = f"IRC {name} has stopped; restarting" | |||||
self.logger.warn(log) | self.logger.warn(log) | ||||
self._dispatch_irc_component(name, klass) | self._dispatch_irc_component(name, klass) | ||||
@@ -151,7 +152,8 @@ class Bot: | |||||
skips = component_names + ["MainThread", "reminder", "irc:quit"] | skips = component_names + ["MainThread", "reminder", "irc:quit"] | ||||
for thread in enumerate_threads(): | for thread in enumerate_threads(): | ||||
if thread.is_alive() and not any( | if thread.is_alive() and not any( | ||||
thread.name.startswith(skip) for skip in skips): | |||||
thread.name.startswith(skip) for skip in skips | |||||
): | |||||
tasks.append(thread.name) | tasks.append(thread.name) | ||||
if tasks: | if tasks: | ||||
log = "The following commands or tasks will be killed: {0}" | log = "The following commands or tasks will be killed: {0}" | ||||
@@ -173,7 +175,7 @@ class Bot: | |||||
ensuring that all components remain online and restarting components | ensuring that all components remain online and restarting components | ||||
that get disconnected from their servers. | that get disconnected from their servers. | ||||
""" | """ | ||||
self.logger.info("Starting bot (EarwigBot {0})".format(__version__)) | |||||
self.logger.info(f"Starting bot (EarwigBot {__version__})") | |||||
self._start_irc_components() | self._start_irc_components() | ||||
self._start_wiki_scheduler() | self._start_wiki_scheduler() | ||||
while self._keep_looping: | while self._keep_looping: | ||||
@@ -195,7 +197,7 @@ class Bot: | |||||
If given, *msg* will be used as our quit message. | If given, *msg* will be used as our quit message. | ||||
""" | """ | ||||
if msg: | if msg: | ||||
self.logger.info('Restarting bot ("{0}")'.format(msg)) | |||||
self.logger.info(f'Restarting bot ("{msg}")') | |||||
else: | else: | ||||
self.logger.info("Restarting bot") | self.logger.info("Restarting bot") | ||||
with self.component_lock: | with self.component_lock: | ||||
@@ -211,7 +213,7 @@ class Bot: | |||||
If given, *msg* will be used as our quit message. | If given, *msg* will be used as our quit message. | ||||
""" | """ | ||||
if msg: | if msg: | ||||
self.logger.info('Stopping bot ("{0}")'.format(msg)) | |||||
self.logger.info(f'Stopping bot ("{msg}")') | |||||
else: | else: | ||||
self.logger.info("Stopping bot") | self.logger.info("Stopping bot") | ||||
with self.component_lock: | with self.component_lock: | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,6 +20,7 @@ | |||||
__all__ = ["Command"] | __all__ = ["Command"] | ||||
class Command: | class Command: | ||||
""" | """ | ||||
**EarwigBot: Base IRC Command** | **EarwigBot: Base IRC Command** | ||||
@@ -36,6 +35,7 @@ class Command: | |||||
This docstring is reported to the user when they type ``"!help | This docstring is reported to the user when they type ``"!help | ||||
<command>"``. | <command>"``. | ||||
""" | """ | ||||
# The command's name, as reported to the user when they use !help: | # The command's name, as reported to the user when they use !help: | ||||
name = None | name = None | ||||
@@ -62,15 +62,31 @@ class Command: | |||||
self.logger = bot.commands.logger.getChild(self.name) | self.logger = bot.commands.logger.getChild(self.name) | ||||
# Convenience functions: | # Convenience functions: | ||||
self.say = lambda target, msg, hidelog=False: self.bot.frontend.say(target, msg, hidelog) | |||||
self.reply = lambda data, msg, hidelog=False: self.bot.frontend.reply(data, msg, hidelog) | |||||
self.action = lambda target, msg, hidelog=False: self.bot.frontend.action(target, msg, hidelog) | |||||
self.notice = lambda target, msg, hidelog=False: self.bot.frontend.notice(target, msg, hidelog) | |||||
self.say = lambda target, msg, hidelog=False: self.bot.frontend.say( | |||||
target, msg, hidelog | |||||
) | |||||
self.reply = lambda data, msg, hidelog=False: self.bot.frontend.reply( | |||||
data, msg, hidelog | |||||
) | |||||
self.action = lambda target, msg, hidelog=False: self.bot.frontend.action( | |||||
target, msg, hidelog | |||||
) | |||||
self.notice = lambda target, msg, hidelog=False: self.bot.frontend.notice( | |||||
target, msg, hidelog | |||||
) | |||||
self.join = lambda chan, hidelog=False: self.bot.frontend.join(chan, hidelog) | self.join = lambda chan, hidelog=False: self.bot.frontend.join(chan, hidelog) | ||||
self.part = lambda chan, msg=None, hidelog=False: self.bot.frontend.part(chan, msg, hidelog) | |||||
self.mode = lambda t, level, msg, hidelog=False: self.bot.frontend.mode(t, level, msg, hidelog) | |||||
self.ping = lambda target, hidelog=False: self.bot.frontend.ping(target, hidelog) | |||||
self.pong = lambda target, hidelog=False: self.bot.frontend.pong(target, hidelog) | |||||
self.part = lambda chan, msg=None, hidelog=False: self.bot.frontend.part( | |||||
chan, msg, hidelog | |||||
) | |||||
self.mode = lambda t, level, msg, hidelog=False: self.bot.frontend.mode( | |||||
t, level, msg, hidelog | |||||
) | |||||
self.ping = lambda target, hidelog=False: self.bot.frontend.ping( | |||||
target, hidelog | |||||
) | |||||
self.pong = lambda target, hidelog=False: self.bot.frontend.pong( | |||||
target, hidelog | |||||
) | |||||
self.setup() | self.setup() | ||||
@@ -81,7 +97,7 @@ class Command: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Command.""" | """Return a nice string representation of the Command.""" | ||||
return "<Command {0} of {1}>".format(self.name, self.bot) | |||||
return f"<Command {self.name} of {self.bot}>" | |||||
def setup(self): | def setup(self): | ||||
"""Hook called immediately after the command is loaded. | """Hook called immediately after the command is loaded. | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,8 +22,10 @@ import re | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Access(Command): | class Access(Command): | ||||
"""Control and get info on who can access the bot.""" | """Control and get info on who can access the bot.""" | ||||
name = "access" | name = "access" | ||||
commands = ["access", "permission", "permissions", "perm", "perms"] | commands = ["access", "permission", "permissions", "perm", "perms"] | ||||
@@ -42,15 +42,15 @@ class Access(Command): | |||||
elif data.args[0] == "help": | elif data.args[0] == "help": | ||||
self.reply(data, "Subcommands are self, list, add, and remove.") | self.reply(data, "Subcommands are self, list, add, and remove.") | ||||
else: | else: | ||||
msg = "Unknown subcommand \x0303{0}\x0F. Subcommands are self, list, add, remove." | |||||
msg = "Unknown subcommand \x0303{0}\x0f. Subcommands are self, list, add, remove." | |||||
self.reply(data, msg.format(data.args[0])) | self.reply(data, msg.format(data.args[0])) | ||||
def do_self(self, data, permdb): | def do_self(self, data, permdb): | ||||
if permdb.is_owner(data): | if permdb.is_owner(data): | ||||
msg = "You are a bot owner (matching rule \x0302{0}\x0F)." | |||||
msg = "You are a bot owner (matching rule \x0302{0}\x0f)." | |||||
self.reply(data, msg.format(permdb.is_owner(data))) | self.reply(data, msg.format(permdb.is_owner(data))) | ||||
elif permdb.is_admin(data): | elif permdb.is_admin(data): | ||||
msg = "You are a bot admin (matching rule \x0302{0}\x0F)." | |||||
msg = "You are a bot admin (matching rule \x0302{0}\x0f)." | |||||
self.reply(data, msg.format(permdb.is_admin(data))) | self.reply(data, msg.format(permdb.is_admin(data))) | ||||
else: | else: | ||||
self.reply(data, "You do not match any bot access rules.") | self.reply(data, "You do not match any bot access rules.") | ||||
@@ -62,18 +62,18 @@ class Access(Command): | |||||
elif data.args[1] in ["admin", "admins"]: | elif data.args[1] in ["admin", "admins"]: | ||||
name, rules = "admins", permdb.users.get(permdb.ADMIN) | name, rules = "admins", permdb.users.get(permdb.ADMIN) | ||||
else: | else: | ||||
msg = "Unknown access level \x0302{0}\x0F." | |||||
msg = "Unknown access level \x0302{0}\x0f." | |||||
self.reply(data, msg.format(data.args[1])) | self.reply(data, msg.format(data.args[1])) | ||||
return | return | ||||
if rules: | if rules: | ||||
msg = "Bot {0}: {1}.".format(name, ", ".join(map(str, rules))) | |||||
msg = "Bot {}: {}.".format(name, ", ".join(map(str, rules))) | |||||
else: | else: | ||||
msg = "No bot {0}.".format(name) | |||||
msg = f"No bot {name}." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
else: | else: | ||||
owners = len(permdb.users.get(permdb.OWNER, [])) | owners = len(permdb.users.get(permdb.OWNER, [])) | ||||
admins = len(permdb.users.get(permdb.ADMIN, [])) | admins = len(permdb.users.get(permdb.ADMIN, [])) | ||||
msg = "There are \x02{0}\x0F bot owners and \x02{1}\x0F bot admins. Use '!{2} list owners' or '!{2} list admins' for details." | |||||
msg = "There are \x02{0}\x0f bot owners and \x02{1}\x0f bot admins. Use '!{2} list owners' or '!{2} list admins' for details." | |||||
self.reply(data, msg.format(owners, admins, data.command)) | self.reply(data, msg.format(owners, admins, data.command)) | ||||
def do_add(self, data, permdb): | def do_add(self, data, permdb): | ||||
@@ -85,12 +85,12 @@ class Access(Command): | |||||
else: | else: | ||||
name, level, adder = "admin", permdb.ADMIN, permdb.add_admin | name, level, adder = "admin", permdb.ADMIN, permdb.add_admin | ||||
if permdb.has_exact(level, nick, ident, host): | if permdb.has_exact(level, nick, ident, host): | ||||
rule = "{0}!{1}@{2}".format(nick, ident, host) | |||||
msg = "\x0302{0}\x0F is already a bot {1}.".format(rule, name) | |||||
rule = f"{nick}!{ident}@{host}" | |||||
msg = f"\x0302{rule}\x0f is already a bot {name}." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
else: | else: | ||||
rule = adder(nick, ident, host) | rule = adder(nick, ident, host) | ||||
msg = "Added bot {0} \x0302{1}\x0F.".format(name, rule) | |||||
msg = f"Added bot {name} \x0302{rule}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_remove(self, data, permdb): | def do_remove(self, data, permdb): | ||||
@@ -103,11 +103,11 @@ class Access(Command): | |||||
name, rmver = "admin", permdb.remove_admin | name, rmver = "admin", permdb.remove_admin | ||||
rule = rmver(nick, ident, host) | rule = rmver(nick, ident, host) | ||||
if rule: | if rule: | ||||
msg = "Removed bot {0} \x0302{1}\x0F.".format(name, rule) | |||||
msg = f"Removed bot {name} \x0302{rule}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
else: | else: | ||||
rule = "{0}!{1}@{2}".format(nick, ident, host) | |||||
msg = "No bot {0} matching \x0302{1}\x0F.".format(name, rule) | |||||
rule = f"{nick}!{ident}@{host}" | |||||
msg = f"No bot {name} matching \x0302{rule}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def get_user_from_args(self, data, permdb): | def get_user_from_args(self, data, permdb): | ||||
@@ -136,6 +136,6 @@ class Access(Command): | |||||
return user.group(1), user.group(2), user.group(3) | return user.group(1), user.group(2), user.group(3) | ||||
def no_arg_error(self, data): | def no_arg_error(self, data): | ||||
msg = 'Please specify a user, either as "\x0302nick\x0F!\x0302ident\x0F@\x0302host\x0F"' | |||||
msg += ' or "nick=\x0302nick\x0F, ident=\x0302ident\x0F, host=\x0302host\x0F".' | |||||
msg = 'Please specify a user, either as "\x0302nick\x0f!\x0302ident\x0f@\x0302host\x0f"' | |||||
msg += ' or "nick=\x0302nick\x0f, ident=\x0302ident\x0f, host=\x0302host\x0f".' | |||||
self.reply(data, msg) | self.reply(data, msg) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,14 +19,16 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import re | import re | ||||
import urllib.request | |||||
import urllib.parse | import urllib.parse | ||||
import urllib.request | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Calc(Command): | class Calc(Command): | ||||
"""A somewhat advanced calculator: see https://futureboy.us/fsp/frink.fsp | """A somewhat advanced calculator: see https://futureboy.us/fsp/frink.fsp | ||||
for details.""" | for details.""" | ||||
name = "calc" | name = "calc" | ||||
def process(self, data): | def process(self, data): | ||||
@@ -36,15 +36,15 @@ class Calc(Command): | |||||
self.reply(data, "What do you want me to calculate?") | self.reply(data, "What do you want me to calculate?") | ||||
return | return | ||||
query = ' '.join(data.args) | |||||
query = " ".join(data.args) | |||||
query = self.cleanup(query) | query = self.cleanup(query) | ||||
url = "https://futureboy.us/fsp/frink.fsp?fromVal={0}" | url = "https://futureboy.us/fsp/frink.fsp?fromVal={0}" | ||||
url = url.format(urllib.parse.quote(query)) | url = url.format(urllib.parse.quote(query)) | ||||
result = urllib.request.urlopen(url).read().decode() | result = urllib.request.urlopen(url).read().decode() | ||||
r_result = re.compile(r'(?i)<A NAME=results>(.*?)</A>') | |||||
r_tag = re.compile(r'<\S+.*?>') | |||||
r_result = re.compile(r"(?i)<A NAME=results>(.*?)</A>") | |||||
r_tag = re.compile(r"<\S+.*?>") | |||||
match = r_result.search(result) | match = r_result.search(result) | ||||
if not match: | if not match: | ||||
@@ -52,32 +52,32 @@ class Calc(Command): | |||||
return | return | ||||
result = match.group(1) | result = match.group(1) | ||||
result = r_tag.sub("", result) # strip span.warning tags | |||||
result = r_tag.sub("", result) # strip span.warning tags | |||||
result = result.replace(">", ">") | result = result.replace(">", ">") | ||||
result = result.replace("(undefined symbol)", "(?) ") | result = result.replace("(undefined symbol)", "(?) ") | ||||
result = result.strip() | result = result.strip() | ||||
if not result: | if not result: | ||||
result = '?' | |||||
result = "?" | |||||
elif " in " in query: | elif " in " in query: | ||||
result += " " + query.split(" in ", 1)[1] | result += " " + query.split(" in ", 1)[1] | ||||
res = "%s = %s" % (query, result) | |||||
res = f"{query} = {result}" | |||||
self.reply(data, res) | self.reply(data, res) | ||||
@staticmethod | @staticmethod | ||||
def cleanup(query): | def cleanup(query): | ||||
fixes = [ | fixes = [ | ||||
(' in ', ' -> '), | |||||
(' over ', ' / '), | |||||
('£', 'GBP '), | |||||
('€', 'EUR '), | |||||
(r'\$', 'USD '), | |||||
(r'\bKB\b', 'kilobytes'), | |||||
(r'\bMB\b', 'megabytes'), | |||||
(r'\bGB\b', 'gigabytes'), | |||||
('kbps', '(kilobits / second)'), | |||||
('mbps', '(megabits / second)') | |||||
(" in ", " -> "), | |||||
(" over ", " / "), | |||||
("£", "GBP "), | |||||
("€", "EUR "), | |||||
(r"\$", "USD "), | |||||
(r"\bKB\b", "kilobytes"), | |||||
(r"\bMB\b", "megabytes"), | |||||
(r"\bGB\b", "gigabytes"), | |||||
("kbps", "(kilobits / second)"), | |||||
("mbps", "(megabits / second)"), | |||||
] | ] | ||||
for original, fix in fixes: | for original, fix in fixes: | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,17 +20,32 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class ChanOps(Command): | class ChanOps(Command): | ||||
"""Voice, devoice, op, or deop users in the channel, or join or part from | """Voice, devoice, op, or deop users in the channel, or join or part from | ||||
other channels.""" | other channels.""" | ||||
name = "chanops" | name = "chanops" | ||||
commands = ["chanops", "voice", "devoice", "op", "deop", "join", "part", "listchans"] | |||||
commands = [ | |||||
"chanops", | |||||
"voice", | |||||
"devoice", | |||||
"op", | |||||
"deop", | |||||
"join", | |||||
"part", | |||||
"listchans", | |||||
] | |||||
def process(self, data): | def process(self, data): | ||||
if data.command == "chanops": | if data.command == "chanops": | ||||
msg = "Available commands are {0}." | msg = "Available commands are {0}." | ||||
self.reply(data, msg.format(", ".join( | |||||
"!" + cmd for cmd in self.commands if cmd != data.command))) | |||||
self.reply( | |||||
data, | |||||
msg.format( | |||||
", ".join("!" + cmd for cmd in self.commands if cmd != data.command) | |||||
), | |||||
) | |||||
return | return | ||||
de_escalate = data.command in ["devoice", "deop"] | de_escalate = data.command in ["devoice", "deop"] | ||||
if de_escalate and (not data.args or data.args[0] == data.nick): | if de_escalate and (not data.args or data.args[0] == data.nick): | ||||
@@ -70,7 +83,7 @@ class ChanOps(Command): | |||||
return | return | ||||
self.join(channel) | self.join(channel) | ||||
log = "{0} requested JOIN to {1}".format(data.nick, channel) | |||||
log = f"{data.nick} requested JOIN to {channel}" | |||||
self.logger.info(log) | self.logger.info(log) | ||||
def do_part(self, data): | def do_part(self, data): | ||||
@@ -85,11 +98,11 @@ class ChanOps(Command): | |||||
else: # "!part reason for parting"; assume current channel | else: # "!part reason for parting"; assume current channel | ||||
reason = " ".join(data.args) | reason = " ".join(data.args) | ||||
msg = "Requested by {0}".format(data.nick) | |||||
log = "{0} requested PART from {1}".format(data.nick, channel) | |||||
msg = f"Requested by {data.nick}" | |||||
log = f"{data.nick} requested PART from {channel}" | |||||
if reason: | if reason: | ||||
msg += ": {0}".format(reason) | |||||
log += ' ("{0}")'.format(reason) | |||||
msg += f": {reason}" | |||||
log += f' ("{reason}")' | |||||
self.part(channel, msg) | self.part(channel, msg) | ||||
self.logger.info(log) | self.logger.info(log) | ||||
@@ -98,5 +111,9 @@ class ChanOps(Command): | |||||
if not chans: | if not chans: | ||||
self.reply(data, "I am currently in no channels.") | self.reply(data, "I am currently in no channels.") | ||||
return | return | ||||
self.reply(data, "I am currently in \x02{0}\x0F channel{1}: {2}.".format( | |||||
len(chans), "" if len(chans) == 1 else "s", ", ".join(sorted(chans)))) | |||||
self.reply( | |||||
data, | |||||
"I am currently in \x02{}\x0f channel{}: {}.".format( | |||||
len(chans), "" if len(chans) == 1 else "s", ", ".join(sorted(chans)) | |||||
), | |||||
) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,23 +18,31 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from collections import namedtuple | |||||
import re | import re | ||||
import socket | import socket | ||||
from collections import namedtuple | |||||
from socket import AF_INET, AF_INET6 | from socket import AF_INET, AF_INET6 | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
_IP = namedtuple("_IP", ["family", "ip", "size"]) | _IP = namedtuple("_IP", ["family", "ip", "size"]) | ||||
_Range = namedtuple("_Range", [ | |||||
"family", "range", "low", "high", "size", "addresses"]) | |||||
_Range = namedtuple("_Range", ["family", "range", "low", "high", "size", "addresses"]) | |||||
class CIDR(Command): | class CIDR(Command): | ||||
"""Calculates the smallest CIDR range that encompasses a list of IP | """Calculates the smallest CIDR range that encompasses a list of IP | ||||
addresses. Used to make range blocks.""" | addresses. Used to make range blocks.""" | ||||
name = "cidr" | name = "cidr" | ||||
commands = ["cidr", "range", "rangeblock", "rangecalc", "blockcalc", | |||||
"iprange", "cdir"] | |||||
commands = [ | |||||
"cidr", | |||||
"range", | |||||
"rangeblock", | |||||
"rangecalc", | |||||
"blockcalc", | |||||
"iprange", | |||||
"cdir", | |||||
] | |||||
# https://www.mediawiki.org/wiki/Manual:$wgBlockCIDRLimit | # https://www.mediawiki.org/wiki/Manual:$wgBlockCIDRLimit | ||||
LIMIT_IPv4 = 16 | LIMIT_IPv4 = 16 | ||||
@@ -44,22 +50,25 @@ class CIDR(Command): | |||||
def process(self, data): | def process(self, data): | ||||
if not data.args: | if not data.args: | ||||
msg = ("Specify a list of IP addresses to calculate a CIDR range " | |||||
"for. For example, \x0306!{0} 192.168.0.3 192.168.0.15 " | |||||
"192.168.1.4\x0F or \x0306!{0} 2500:1:2:3:: " | |||||
"2500:1:2:3:dead:beef::\x0F.") | |||||
msg = ( | |||||
"Specify a list of IP addresses to calculate a CIDR range " | |||||
"for. For example, \x0306!{0} 192.168.0.3 192.168.0.15 " | |||||
"192.168.1.4\x0f or \x0306!{0} 2500:1:2:3:: " | |||||
"2500:1:2:3:dead:beef::\x0f." | |||||
) | |||||
self.reply(data, msg.format(data.command)) | self.reply(data, msg.format(data.command)) | ||||
return | return | ||||
try: | try: | ||||
ips = [self._parse_ip(arg) for arg in data.args] | ips = [self._parse_ip(arg) for arg in data.args] | ||||
except ValueError as exc: | except ValueError as exc: | ||||
msg = "Can't parse IP address \x0302{0}\x0F." | |||||
msg = "Can't parse IP address \x0302{0}\x0f." | |||||
self.reply(data, msg.format(exc.message)) | self.reply(data, msg.format(exc.message)) | ||||
return | return | ||||
if any(ip.family == AF_INET for ip in ips) and any( | if any(ip.family == AF_INET for ip in ips) and any( | ||||
ip.family == AF_INET6 for ip in ips): | |||||
ip.family == AF_INET6 for ip in ips | |||||
): | |||||
msg = "Can't calculate a range for both IPv4 and IPv6 addresses." | msg = "Can't calculate a range for both IPv4 and IPv6 addresses." | ||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
@@ -67,11 +76,20 @@ class CIDR(Command): | |||||
cidr = self._calculate_range(ips[0].family, ips) | cidr = self._calculate_range(ips[0].family, ips) | ||||
descr = self._describe(cidr.family, cidr.size) | descr = self._describe(cidr.family, cidr.size) | ||||
msg = ("Smallest CIDR range is \x02{0}\x0F, covering {1} from " | |||||
"\x0305{2}\x0F to \x0305{3}\x0F{4}.") | |||||
self.reply(data, msg.format( | |||||
cidr.range, cidr.addresses, cidr.low, cidr.high, | |||||
" (\x0304{0}\x0F)".format(descr) if descr else "")) | |||||
msg = ( | |||||
"Smallest CIDR range is \x02{0}\x0f, covering {1} from " | |||||
"\x0305{2}\x0f to \x0305{3}\x0f{4}." | |||||
) | |||||
self.reply( | |||||
data, | |||||
msg.format( | |||||
cidr.range, | |||||
cidr.addresses, | |||||
cidr.low, | |||||
cidr.high, | |||||
f" (\x0304{descr}\x0f)" if descr else "", | |||||
), | |||||
) | |||||
def _parse_ip(self, arg): | def _parse_ip(self, arg): | ||||
"""Converts an argument into an IP address object.""" | """Converts an argument into an IP address object.""" | ||||
@@ -89,10 +107,10 @@ class CIDR(Command): | |||||
try: | try: | ||||
ip = _IP(AF_INET, socket.inet_pton(AF_INET, arg), size) | ip = _IP(AF_INET, socket.inet_pton(AF_INET, arg), size) | ||||
except socket.error: | |||||
except OSError: | |||||
try: | try: | ||||
return _IP(AF_INET6, socket.inet_pton(AF_INET6, arg), size) | return _IP(AF_INET6, socket.inet_pton(AF_INET6, arg), size) | ||||
except socket.error: | |||||
except OSError: | |||||
raise ValueError(oldarg) | raise ValueError(oldarg) | ||||
if size > 32: | if size > 32: | ||||
raise ValueError(oldarg) | raise ValueError(oldarg) | ||||
@@ -126,18 +144,20 @@ class CIDR(Command): | |||||
def _calculate_range(self, family, ips): | def _calculate_range(self, family, ips): | ||||
"""Calculate the smallest CIDR range encompassing a list of IPs.""" | """Calculate the smallest CIDR range encompassing a list of IPs.""" | ||||
bin_ips = ["".join( | |||||
bin(ord(octet))[2:].zfill(8) for octet in ip.ip) for ip in ips] | |||||
bin_ips = [ | |||||
"".join(bin(ord(octet))[2:].zfill(8) for octet in ip.ip) for ip in ips | |||||
] | |||||
for i, ip in enumerate(ips): | for i, ip in enumerate(ips): | ||||
if ip.size is not None: | if ip.size is not None: | ||||
suffix = "X" * (len(bin_ips[i]) - ip.size) | suffix = "X" * (len(bin_ips[i]) - ip.size) | ||||
bin_ips[i] = bin_ips[i][:ip.size] + suffix | |||||
bin_ips[i] = bin_ips[i][: ip.size] + suffix | |||||
size = len(bin_ips[0]) | size = len(bin_ips[0]) | ||||
for i in range(len(bin_ips[0])): | for i in range(len(bin_ips[0])): | ||||
if any(ip[i] == "X" for ip in bin_ips) or ( | if any(ip[i] == "X" for ip in bin_ips) or ( | ||||
any(ip[i] == "0" for ip in bin_ips) and | |||||
any(ip[i] == "1" for ip in bin_ips)): | |||||
any(ip[i] == "0" for ip in bin_ips) | |||||
and any(ip[i] == "1" for ip in bin_ips) | |||||
): | |||||
size = i | size = i | ||||
break | break | ||||
@@ -147,33 +167,41 @@ class CIDR(Command): | |||||
high = self._format_bin(family, bin_high) | high = self._format_bin(family, bin_high) | ||||
return _Range( | return _Range( | ||||
family, low + "/" + str(size), low, high, size, | |||||
self._format_count(2 ** (len(bin_ips[0]) - size))) | |||||
family, | |||||
low + "/" + str(size), | |||||
low, | |||||
high, | |||||
size, | |||||
self._format_count(2 ** (len(bin_ips[0]) - size)), | |||||
) | |||||
@staticmethod | @staticmethod | ||||
def _format_bin(family, binary): | def _format_bin(family, binary): | ||||
"""Convert an IP's binary representation to presentation format.""" | """Convert an IP's binary representation to presentation format.""" | ||||
return socket.inet_ntop(family, "".join( | |||||
chr(int(binary[i:i + 8], 2)) for i in range(0, len(binary), 8))) | |||||
return socket.inet_ntop( | |||||
family, | |||||
"".join(chr(int(binary[i : i + 8], 2)) for i in range(0, len(binary), 8)), | |||||
) | |||||
@staticmethod | @staticmethod | ||||
def _format_count(count): | def _format_count(count): | ||||
"""Nicely format a number of addresses affected by a range block.""" | """Nicely format a number of addresses affected by a range block.""" | ||||
if count == 1: | if count == 1: | ||||
return "1 address" | return "1 address" | ||||
if count > 2 ** 32: | |||||
base = "{0:.2E} addresses".format(count) | |||||
if count == 2 ** 64: | |||||
if count > 2**32: | |||||
base = f"{count:.2E} addresses" | |||||
if count == 2**64: | |||||
return base + " (1 /64 subnet)" | return base + " (1 /64 subnet)" | ||||
if count > 2 ** 96: | |||||
return base + " ({0:.2E} /64 subnets)".format(count >> 64) | |||||
if count > 2 ** 63: | |||||
return base + " ({0:,} /64 subnets)".format(count >> 64) | |||||
if count > 2**96: | |||||
return base + f" ({count >> 64:.2E} /64 subnets)" | |||||
if count > 2**63: | |||||
return base + f" ({count >> 64:,} /64 subnets)" | |||||
return base | return base | ||||
return "{0:,} addresses".format(count) | |||||
return f"{count:,} addresses" | |||||
def _describe(self, family, size): | def _describe(self, family, size): | ||||
"""Return an optional English description of a range.""" | """Return an optional English description of a range.""" | ||||
if (family == AF_INET and size < self.LIMIT_IPv4) or ( | if (family == AF_INET and size < self.LIMIT_IPv4) or ( | ||||
family == AF_INET6 and size < self.LIMIT_IPv6): | |||||
family == AF_INET6 and size < self.LIMIT_IPv6 | |||||
): | |||||
return "too large to block" | return "too large to block" |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -31,9 +29,11 @@ fernet = importer.new("cryptography.fernet") | |||||
hashes = importer.new("cryptography.hazmat.primitives.hashes") | hashes = importer.new("cryptography.hazmat.primitives.hashes") | ||||
pbkdf2 = importer.new("cryptography.hazmat.primitives.kdf.pbkdf2") | pbkdf2 = importer.new("cryptography.hazmat.primitives.kdf.pbkdf2") | ||||
class Crypt(Command): | class Crypt(Command): | ||||
"""Provides hash functions with !hash (!hash list for supported algorithms) | """Provides hash functions with !hash (!hash list for supported algorithms) | ||||
and basic encryption with !encrypt and !decrypt.""" | and basic encryption with !encrypt and !decrypt.""" | ||||
name = "crypt" | name = "crypt" | ||||
commands = ["crypt", "hash", "encrypt", "decrypt"] | commands = ["crypt", "hash", "encrypt", "decrypt"] | ||||
@@ -44,22 +44,22 @@ class Crypt(Command): | |||||
return | return | ||||
if not data.args: | if not data.args: | ||||
msg = "What do you want me to {0}?".format(data.command) | |||||
msg = f"What do you want me to {data.command}?" | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
if data.command == "hash": | if data.command == "hash": | ||||
algo = data.args[0] | algo = data.args[0] | ||||
if algo == "list": | if algo == "list": | ||||
algos = ', '.join(hashlib.algorithms_available) | |||||
algos = ", ".join(hashlib.algorithms_available) | |||||
msg = algos.join(("Supported algorithms: ", ".")) | msg = algos.join(("Supported algorithms: ", ".")) | ||||
self.reply(data, msg) | self.reply(data, msg) | ||||
elif algo in hashlib.algorithms_available: | elif algo in hashlib.algorithms_available: | ||||
string = ' '.join(data.args[1:]) | |||||
string = " ".join(data.args[1:]) | |||||
result = getattr(hashlib, algo)(string.encode()).hexdigest() | result = getattr(hashlib, algo)(string.encode()).hexdigest() | ||||
self.reply(data, result) | self.reply(data, result) | ||||
else: | else: | ||||
msg = "Unknown algorithm: '{0}'.".format(algo) | |||||
msg = f"Unknown algorithm: '{algo}'." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
else: | else: | ||||
@@ -81,7 +81,9 @@ class Crypt(Command): | |||||
salt=salt, | salt=salt, | ||||
iterations=100000, | iterations=100000, | ||||
) | ) | ||||
f = fernet.Fernet(base64.urlsafe_b64encode(kdf.derive(key.encode()))) | |||||
f = fernet.Fernet( | |||||
base64.urlsafe_b64encode(kdf.derive(key.encode())) | |||||
) | |||||
ciphertext = f.encrypt(text.encode()) | ciphertext = f.encrypt(text.encode()) | ||||
self.reply(data, base64.b64encode(salt + ciphertext).decode()) | self.reply(data, base64.b64encode(salt + ciphertext).decode()) | ||||
else: | else: | ||||
@@ -95,9 +97,14 @@ class Crypt(Command): | |||||
salt=salt, | salt=salt, | ||||
iterations=100000, | iterations=100000, | ||||
) | ) | ||||
f = fernet.Fernet(base64.urlsafe_b64encode(kdf.derive(key.encode()))) | |||||
f = fernet.Fernet( | |||||
base64.urlsafe_b64encode(kdf.derive(key.encode())) | |||||
) | |||||
self.reply(data, f.decrypt(ciphertext).decode()) | self.reply(data, f.decrypt(ciphertext).decode()) | ||||
except ImportError: | except ImportError: | ||||
self.reply(data, "This command requires the 'cryptography' package: https://cryptography.io/") | |||||
self.reply( | |||||
data, | |||||
"This command requires the 'cryptography' package: https://cryptography.io/", | |||||
) | |||||
except Exception as error: | except Exception as error: | ||||
self.reply(data, "{}: {}".format(type(error).__name__, str(error))) | |||||
self.reply(data, f"{type(error).__name__}: {str(error)}") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -26,9 +24,11 @@ import time | |||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class CTCP(Command): | class CTCP(Command): | ||||
"""Not an actual command; this module implements responses to the CTCP | """Not an actual command; this module implements responses to the CTCP | ||||
requests PING, TIME, and VERSION.""" | requests PING, TIME, and VERSION.""" | ||||
name = "ctcp" | name = "ctcp" | ||||
hooks = ["msg_private"] | hooks = ["msg_private"] | ||||
@@ -52,17 +52,17 @@ class CTCP(Command): | |||||
if command == "PING": | if command == "PING": | ||||
msg = " ".join(data.line[4:]) | msg = " ".join(data.line[4:]) | ||||
if msg: | if msg: | ||||
self.notice(target, "\x01PING {0}\x01".format(msg)) | |||||
self.notice(target, f"\x01PING {msg}\x01") | |||||
else: | else: | ||||
self.notice(target, "\x01PING\x01") | self.notice(target, "\x01PING\x01") | ||||
elif command == "TIME": | elif command == "TIME": | ||||
ts = time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime()) | ts = time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime()) | ||||
self.notice(target, "\x01TIME {0}\x01".format(ts)) | |||||
self.notice(target, f"\x01TIME {ts}\x01") | |||||
elif command == "VERSION": | elif command == "VERSION": | ||||
default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | ||||
vers = self.config.irc.get("version", default) | vers = self.config.irc.get("version", default) | ||||
vers = vers.replace("$1", __version__) | vers = vers.replace("$1", __version__) | ||||
vers = vers.replace("$2", platform.python_version()) | vers = vers.replace("$2", platform.python_version()) | ||||
self.notice(target, "\x01VERSION {0}\x01".format(vers)) | |||||
self.notice(target, f"\x01VERSION {vers}\x01") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,8 +23,10 @@ import re | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Dictionary(Command): | class Dictionary(Command): | ||||
"""Define words and stuff.""" | """Define words and stuff.""" | ||||
name = "dictionary" | name = "dictionary" | ||||
commands = ["dict", "dictionary", "define", "def"] | commands = ["dict", "dictionary", "define", "def"] | ||||
@@ -63,7 +63,7 @@ class Dictionary(Command): | |||||
level, languages = self.get_languages(entry) | level, languages = self.get_languages(entry) | ||||
if not languages: | if not languages: | ||||
return "Couldn't parse {0}!".format(page.url) | |||||
return f"Couldn't parse {page.url}!" | |||||
if "#" in term: # Requesting a specific language | if "#" in term: # Requesting a specific language | ||||
lcase_langs = {lang.lower(): lang for lang in languages} | lcase_langs = {lang.lower(): lang for lang in languages} | ||||
@@ -73,12 +73,12 @@ class Dictionary(Command): | |||||
resp = "Language {0} not found in definition." | resp = "Language {0} not found in definition." | ||||
return resp.format(request) | return resp.format(request) | ||||
definition = self.get_definition(languages[lang], level) | definition = self.get_definition(languages[lang], level) | ||||
return "({0}) {1}".format(lang, definition) | |||||
return f"({lang}) {definition}" | |||||
result = [] | result = [] | ||||
for lang, section in sorted(languages.items()): | for lang, section in sorted(languages.items()): | ||||
definition = self.get_definition(section, level) | definition = self.get_definition(section, level) | ||||
result.append("({0}) {1}".format(lang, definition)) | |||||
result.append(f"({lang}) {definition}") | |||||
return "; ".join(result) | return "; ".join(result) | ||||
def get_languages(self, entry, level=2): | def get_languages(self, entry, level=2): | ||||
@@ -119,19 +119,22 @@ class Dictionary(Command): | |||||
blocks = "=" * (level + 1) | blocks = "=" * (level + 1) | ||||
defs = [] | defs = [] | ||||
for part, basename in parts_of_speech.items(): | for part, basename in parts_of_speech.items(): | ||||
fullnames = [basename, r"\{\{" + basename + r"\}\}", | |||||
r"\{\{" + basename.lower() + r"\}\}"] | |||||
fullnames = [ | |||||
basename, | |||||
r"\{\{" + basename + r"\}\}", | |||||
r"\{\{" + basename.lower() + r"\}\}", | |||||
] | |||||
for fullname in fullnames: | for fullname in fullnames: | ||||
regex = blocks + r"\s*" + fullname + r"\s*" + blocks | regex = blocks + r"\s*" + fullname + r"\s*" + blocks | ||||
if re.search(regex, section): | if re.search(regex, section): | ||||
regex = blocks + r"\s*" + fullname | regex = blocks + r"\s*" + fullname | ||||
regex += r"\s*{0}(.*?)(?:(?:{0})|\Z)".format(blocks) | |||||
regex += rf"\s*{blocks}(.*?)(?:(?:{blocks})|\Z)" | |||||
bodies = re.findall(regex, section, re.DOTALL) | bodies = re.findall(regex, section, re.DOTALL) | ||||
if bodies: | if bodies: | ||||
for body in bodies: | for body in bodies: | ||||
definition = self.parse_body(body) | definition = self.parse_body(body) | ||||
if definition: | if definition: | ||||
msg = "\x02{0}\x0F {1}" | |||||
msg = "\x02{0}\x0f {1}" | |||||
defs.append(msg.format(part, definition)) | defs.append(msg.format(part, definition)) | ||||
return "; ".join(defs) | return "; ".join(defs) | ||||
@@ -167,7 +170,7 @@ class Dictionary(Command): | |||||
result = [] # Number the senses incrementally | result = [] # Number the senses incrementally | ||||
for i, sense in enumerate(senses): | for i, sense in enumerate(senses): | ||||
result.append("{0}. {1}".format(i + 1, sense)) | |||||
result.append(f"{i + 1}. {sense}") | |||||
return " ".join(result) | return " ".join(result) | ||||
def strip_templates(self, line): | def strip_templates(self, line): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,8 +23,10 @@ from urllib.parse import quote_plus | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Editcount(Command): | class Editcount(Command): | ||||
"""Return a user's edit count.""" | """Return a user's edit count.""" | ||||
name = "editcount" | name = "editcount" | ||||
commands = ["ec", "editcount"] | commands = ["ec", "editcount"] | ||||
@@ -34,7 +34,7 @@ class Editcount(Command): | |||||
if not data.args: | if not data.args: | ||||
name = data.nick | name = data.nick | ||||
else: | else: | ||||
name = ' '.join(data.args) | |||||
name = " ".join(data.args) | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
user = site.get_user(name) | user = site.get_user(name) | ||||
@@ -42,12 +42,12 @@ class Editcount(Command): | |||||
try: | try: | ||||
count = user.editcount | count = user.editcount | ||||
except exceptions.UserNotFoundError: | except exceptions.UserNotFoundError: | ||||
msg = "The user \x0302{0}\x0F does not exist." | |||||
msg = "The user \x0302{0}\x0f does not exist." | |||||
self.reply(data, msg.format(name)) | self.reply(data, msg.format(name)) | ||||
return | return | ||||
safe = quote_plus(user.name.encode("utf8")) | safe = quote_plus(user.name.encode("utf8")) | ||||
url = "https://xtools.wmflabs.org/ec/{}/{}".format(site.domain, safe) | |||||
url = f"https://xtools.wmflabs.org/ec/{site.domain}/{safe}" | |||||
fullurl = url.format(safe, site.domain) | fullurl = url.format(safe, site.domain) | ||||
msg = "\x0302{0}\x0F has {1} edits ({2})." | |||||
msg = "\x0302{0}\x0f has {1} edits ({2})." | |||||
self.reply(data, msg.format(name, count, fullurl)) | self.reply(data, msg.format(name, count, fullurl)) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,14 +18,16 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from platform import python_version | |||||
import re | import re | ||||
from platform import python_version | |||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Help(Command): | class Help(Command): | ||||
"""Displays information about the bot.""" | """Displays information about the bot.""" | ||||
name = "help" | name = "help" | ||||
commands = ["help", "version"] | commands = ["help", "version"] | ||||
@@ -53,7 +53,7 @@ class Help(Command): | |||||
"""Give the user a general help message with a list of all commands.""" | """Give the user a general help message with a list of all commands.""" | ||||
msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'." | msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'." | ||||
cmnds = sorted([cmnd.name for cmnd in self.bot.commands]) | cmnds = sorted([cmnd.name for cmnd in self.bot.commands]) | ||||
msg = msg.format(len(cmnds), ', '.join(cmnds)) | |||||
msg = msg.format(len(cmnds), ", ".join(cmnds)) | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_command_help(self, data): | def do_command_help(self, data): | ||||
@@ -65,16 +65,18 @@ class Help(Command): | |||||
if command.__doc__: | if command.__doc__: | ||||
doc = command.__doc__.replace("\n", "") | doc = command.__doc__.replace("\n", "") | ||||
doc = re.sub(r"\s\s+", " ", doc) | doc = re.sub(r"\s\s+", " ", doc) | ||||
msg = 'Help for command \x0303{0}\x0F: "{1}"' | |||||
msg = 'Help for command \x0303{0}\x0f: "{1}"' | |||||
self.reply(data, msg.format(target, doc)) | self.reply(data, msg.format(target, doc)) | ||||
return | return | ||||
msg = "Sorry, no help for \x0303{0}\x0F.".format(target) | |||||
msg = f"Sorry, no help for \x0303{target}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_hello(self, data): | def do_hello(self, data): | ||||
self.say(data.chan, "Yes, {0}?".format(data.nick)) | |||||
self.say(data.chan, f"Yes, {data.nick}?") | |||||
def do_version(self, data): | def do_version(self, data): | ||||
vers = "EarwigBot v{bot} on Python {python}: https://github.com/earwig/earwigbot" | |||||
vers = ( | |||||
"EarwigBot v{bot} on Python {python}: https://github.com/earwig/earwigbot" | |||||
) | |||||
self.reply(data, vers.format(bot=__version__, python=python_version())) | self.reply(data, vers.format(bot=__version__, python=python_version())) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -23,8 +21,10 @@ | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Lag(Command): | class Lag(Command): | ||||
"""Return replag or maxlag information on specific databases.""" | """Return replag or maxlag information on specific databases.""" | ||||
name = "lag" | name = "lag" | ||||
commands = ["lag", "replag", "maxlag"] | commands = ["lag", "replag", "maxlag"] | ||||
@@ -33,22 +33,21 @@ class Lag(Command): | |||||
if not site: | if not site: | ||||
return | return | ||||
if data.command == "replag": | if data.command == "replag": | ||||
base = "\x0302{0}\x0F: {1}." | |||||
base = "\x0302{0}\x0f: {1}." | |||||
msg = base.format(site.name, self.get_replag(site)) | msg = base.format(site.name, self.get_replag(site)) | ||||
elif data.command == "maxlag": | elif data.command == "maxlag": | ||||
base = "\x0302{0}\x0F: {1}." | |||||
base = "\x0302{0}\x0f: {1}." | |||||
msg = base.format(site.name, self.get_maxlag(site)) | msg = base.format(site.name, self.get_maxlag(site)) | ||||
else: | else: | ||||
base = "\x0302{0}\x0F: {1}; {2}." | |||||
msg = base.format(site.name, self.get_replag(site), | |||||
self.get_maxlag(site)) | |||||
base = "\x0302{0}\x0f: {1}; {2}." | |||||
msg = base.format(site.name, self.get_replag(site), self.get_maxlag(site)) | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def get_replag(self, site): | def get_replag(self, site): | ||||
return "SQL replag is {0}".format(self.time(site.get_replag())) | |||||
return f"SQL replag is {self.time(site.get_replag())}" | |||||
def get_maxlag(self, site): | def get_maxlag(self, site): | ||||
return "API maxlag is {0}".format(self.time(site.get_maxlag())) | |||||
return f"API maxlag is {self.time(site.get_maxlag())}" | |||||
def get_site(self, data): | def get_site(self, data): | ||||
if data.kwargs and "project" in data.kwargs and "lang" in data.kwargs: | if data.kwargs and "project" in data.kwargs and "lang" in data.kwargs: | ||||
@@ -60,7 +59,7 @@ class Lag(Command): | |||||
if len(data.args) > 1: | if len(data.args) > 1: | ||||
name = " ".join(data.args) | name = " ".join(data.args) | ||||
self.reply(data, "Unknown site: \x0302{0}\x0F.".format(name)) | |||||
self.reply(data, f"Unknown site: \x0302{name}\x0f.") | |||||
return | return | ||||
name = data.args[0] | name = data.args[0] | ||||
if "." in name: | if "." in name: | ||||
@@ -71,7 +70,7 @@ class Lag(Command): | |||||
try: | try: | ||||
return self.bot.wiki.get_site(name) | return self.bot.wiki.get_site(name) | ||||
except exceptions.SiteNotFoundError: | except exceptions.SiteNotFoundError: | ||||
msg = "Unknown site: \x0302{0}\x0F.".format(name) | |||||
msg = f"Unknown site: \x0302{name}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
return self.get_site_from_proj_and_lang(data, project, lang) | return self.get_site_from_proj_and_lang(data, project, lang) | ||||
@@ -83,19 +82,24 @@ class Lag(Command): | |||||
try: | try: | ||||
site = self.bot.wiki.add_site(project=project, lang=lang) | site = self.bot.wiki.add_site(project=project, lang=lang) | ||||
except exceptions.APIError: | except exceptions.APIError: | ||||
msg = "Site \x0302{0}:{1}\x0F not found." | |||||
msg = "Site \x0302{0}:{1}\x0f not found." | |||||
self.reply(data, msg.format(project, lang)) | self.reply(data, msg.format(project, lang)) | ||||
return | return | ||||
return site | return site | ||||
def time(self, seconds): | def time(self, seconds): | ||||
parts = [("year", 31536000), ("day", 86400), ("hour", 3600), | |||||
("minute", 60), ("second", 1)] | |||||
parts = [ | |||||
("year", 31536000), | |||||
("day", 86400), | |||||
("hour", 3600), | |||||
("minute", 60), | |||||
("second", 1), | |||||
] | |||||
msg = [] | msg = [] | ||||
for name, size in parts: | for name, size in parts: | ||||
num = seconds / size | num = seconds / size | ||||
seconds -= num * size | seconds -= num * size | ||||
if num: | if num: | ||||
chunk = "{0} {1}".format(num, name if num == 1 else name + "s") | |||||
chunk = "{} {}".format(num, name if num == 1 else name + "s") | |||||
msg.append(chunk) | msg.append(chunk) | ||||
return ", ".join(msg) if msg else "0 seconds" | return ", ".join(msg) if msg else "0 seconds" |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,9 +20,11 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Langcode(Command): | class Langcode(Command): | ||||
"""Convert a language code into its name (or vice versa), and give a list | """Convert a language code into its name (or vice versa), and give a list | ||||
of WMF sites in that language.""" | of WMF sites in that language.""" | ||||
name = "langcode" | name = "langcode" | ||||
commands = ["langcode", "lang", "language"] | commands = ["langcode", "lang", "language"] | ||||
@@ -46,17 +46,17 @@ class Langcode(Command): | |||||
localname = site["localname"].encode("utf8") | localname = site["localname"].encode("utf8") | ||||
if site["code"] == lcase: | if site["code"] == lcase: | ||||
if name != localname: | if name != localname: | ||||
name += " ({0})".format(localname) | |||||
name += f" ({localname})" | |||||
sites = ", ".join([s["url"] for s in site["site"]]) | sites = ", ".join([s["url"] for s in site["site"]]) | ||||
msg = "\x0302{0}\x0F is {1} ({2})".format(code, name, sites) | |||||
msg = f"\x0302{code}\x0f is {name} ({sites})" | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
elif name.lower() == lcase or localname.lower() == lcase: | elif name.lower() == lcase or localname.lower() == lcase: | ||||
if name != localname: | if name != localname: | ||||
name += " ({0})".format(localname) | |||||
name += f" ({localname})" | |||||
sites = ", ".join([s["url"] for s in site["site"]]) | sites = ", ".join([s["url"] for s in site["site"]]) | ||||
msg = "{0} is \x0302{1}\x0F ({2})" | |||||
msg = "{0} is \x0302{1}\x0f ({2})" | |||||
self.reply(data, msg.format(name, site["code"], sites)) | self.reply(data, msg.format(name, site["code"], sites)) | ||||
return | return | ||||
self.reply(data, "Language \x0302{0}\x0F not found.".format(code)) | |||||
self.reply(data, f"Language \x0302{code}\x0f not found.") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,8 +22,10 @@ import re | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Link(Command): | class Link(Command): | ||||
"""Convert a Wikipedia page name into a URL.""" | """Convert a Wikipedia page name into a URL.""" | ||||
name = "link" | name = "link" | ||||
def setup(self): | def setup(self): | ||||
@@ -72,7 +72,10 @@ class Link(Command): | |||||
# Find all {{templates}} | # Find all {{templates}} | ||||
templates = re.findall(r"(\{\{(.*?)(\||\}\}))", line) | templates = re.findall(r"(\{\{(.*?)(\||\}\}))", line) | ||||
if templates: | if templates: | ||||
p_tmpl = lambda name: self.site.get_page("Template:" + name).url | |||||
def p_tmpl(name): | |||||
return self.site.get_page("Template:" + name).url | |||||
templates = [p_tmpl(i[1]) for i in templates] | templates = [p_tmpl(i[1]) for i in templates] | ||||
results += templates | results += templates | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,16 +18,18 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from datetime import datetime | |||||
from os import path | |||||
import re | import re | ||||
import sqlite3 as sqlite | import sqlite3 as sqlite | ||||
from datetime import datetime | |||||
from os import path | |||||
from threading import Lock | from threading import Lock | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Notes(Command): | class Notes(Command): | ||||
"""A mini IRC-based wiki for storing notes, tips, and reminders.""" | """A mini IRC-based wiki for storing notes, tips, and reminders.""" | ||||
name = "notes" | name = "notes" | ||||
commands = ["notes", "note", "about"] | commands = ["notes", "note", "about"] | ||||
version = "2.1" | version = "2.1" | ||||
@@ -43,7 +43,7 @@ class Notes(Command): | |||||
"change": "edit", | "change": "edit", | ||||
"modify": "edit", | "modify": "edit", | ||||
"move": "rename", | "move": "rename", | ||||
"remove": "delete" | |||||
"remove": "delete", | |||||
} | } | ||||
def setup(self): | def setup(self): | ||||
@@ -70,7 +70,7 @@ class Notes(Command): | |||||
elif command in self.aliases: | elif command in self.aliases: | ||||
commands[self.aliases[command]](data) | commands[self.aliases[command]](data) | ||||
else: | else: | ||||
msg = "Unknown subcommand: \x0303{0}\x0F.".format(command) | |||||
msg = f"Unknown subcommand: \x0303{command}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_help(self, data): | def do_help(self, data): | ||||
@@ -94,8 +94,10 @@ class Notes(Command): | |||||
try: | try: | ||||
command = data.args[1] | command = data.args[1] | ||||
except IndexError: | except IndexError: | ||||
msg = ("\x0302The Earwig Mini-Wiki\x0F: running v{0}. Subcommands " | |||||
"are: {1}. You can get help on any with '!{2} help subcommand'.") | |||||
msg = ( | |||||
"\x0302The Earwig Mini-Wiki\x0f: running v{0}. Subcommands " | |||||
"are: {1}. You can get help on any with '!{2} help subcommand'." | |||||
) | |||||
cmnds = ", ".join(info.keys()) | cmnds = ", ".join(info.keys()) | ||||
self.reply(data, msg.format(self.version, cmnds, data.command)) | self.reply(data, msg.format(self.version, cmnds, data.command)) | ||||
return | return | ||||
@@ -103,9 +105,9 @@ class Notes(Command): | |||||
command = self.aliases[command] | command = self.aliases[command] | ||||
try: | try: | ||||
help_ = re.sub(r"\s\s+", " ", info[command].replace("\n", "")) | help_ = re.sub(r"\s\s+", " ", info[command].replace("\n", "")) | ||||
self.reply(data, "\x0303{0}\x0F: ".format(command) + help_) | |||||
self.reply(data, f"\x0303{command}\x0f: " + help_) | |||||
except KeyError: | except KeyError: | ||||
msg = "Unknown subcommand: \x0303{0}\x0F.".format(command) | |||||
msg = f"Unknown subcommand: \x0303{command}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_list(self, data): | def do_list(self, data): | ||||
@@ -119,7 +121,7 @@ class Notes(Command): | |||||
if entries: | if entries: | ||||
entries = [entry[0].encode("utf8") for entry in entries] | entries = [entry[0].encode("utf8") for entry in entries] | ||||
self.reply(data, "Entries: {0}".format(", ".join(entries))) | |||||
self.reply(data, "Entries: {}".format(", ".join(entries))) | |||||
else: | else: | ||||
self.reply(data, "No entries in the database.") | self.reply(data, "No entries in the database.") | ||||
@@ -142,10 +144,10 @@ class Notes(Command): | |||||
title = title.encode("utf8") | title = title.encode("utf8") | ||||
if content: | if content: | ||||
msg = "\x0302{0}\x0F: {1}" | |||||
msg = "\x0302{0}\x0f: {1}" | |||||
self.reply(data, msg.format(title, content.encode("utf8"))) | self.reply(data, msg.format(title, content.encode("utf8"))) | ||||
else: | else: | ||||
self.reply(data, "Entry \x0302{0}\x0F not found.".format(title)) | |||||
self.reply(data, f"Entry \x0302{title}\x0f not found.") | |||||
def do_edit(self, data): | def do_edit(self, data): | ||||
"""Edit an entry in the notes database.""" | """Edit an entry in the notes database.""" | ||||
@@ -191,7 +193,7 @@ class Notes(Command): | |||||
else: | else: | ||||
conn.execute(query4, (revid, id_)) | conn.execute(query4, (revid, id_)) | ||||
msg = "Entry \x0302{0}\x0F updated." | |||||
msg = "Entry \x0302{0}\x0f updated." | |||||
self.reply(data, msg.format(title.encode("utf8"))) | self.reply(data, msg.format(title.encode("utf8"))) | ||||
def do_info(self, data): | def do_info(self, data): | ||||
@@ -216,17 +218,17 @@ class Notes(Command): | |||||
title = info[0][0] | title = info[0][0] | ||||
times = [datum[1] for datum in info] | times = [datum[1] for datum in info] | ||||
earliest = min(times) | earliest = min(times) | ||||
msg = "\x0302{0}\x0F: {1} edits since {2}" | |||||
msg = "\x0302{0}\x0f: {1} edits since {2}" | |||||
msg = msg.format(title.encode("utf8"), len(info), earliest) | msg = msg.format(title.encode("utf8"), len(info), earliest) | ||||
if len(times) > 1: | if len(times) > 1: | ||||
latest = max(times) | latest = max(times) | ||||
msg += "; last edit on {0}".format(latest) | |||||
msg += f"; last edit on {latest}" | |||||
names = [datum[2] for datum in info] | names = [datum[2] for datum in info] | ||||
msg += "; authors: {0}.".format(", ".join(list(set(names)))) | |||||
msg += "; authors: {}.".format(", ".join(list(set(names)))) | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
else: | else: | ||||
title = data.args[1] | title = data.args[1] | ||||
self.reply(data, "Entry \x0302{0}\x0F not found.".format(title)) | |||||
self.reply(data, f"Entry \x0302{title}\x0f not found.") | |||||
def do_rename(self, data): | def do_rename(self, data): | ||||
"""Rename an entry in the notes database.""" | """Rename an entry in the notes database.""" | ||||
@@ -254,7 +256,7 @@ class Notes(Command): | |||||
try: | try: | ||||
id_, author = conn.execute(query1, (slug,)).fetchone() | id_, author = conn.execute(query1, (slug,)).fetchone() | ||||
except (sqlite.OperationalError, TypeError): | except (sqlite.OperationalError, TypeError): | ||||
msg = "Entry \x0302{0}\x0F not found.".format(data.args[1]) | |||||
msg = f"Entry \x0302{data.args[1]}\x0f not found." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
permdb = self.config.irc["permissions"] | permdb = self.config.irc["permissions"] | ||||
@@ -265,7 +267,7 @@ class Notes(Command): | |||||
args = (self._slugify(newtitle), newtitle.decode("utf8"), id_) | args = (self._slugify(newtitle), newtitle.decode("utf8"), id_) | ||||
conn.execute(query2, args) | conn.execute(query2, args) | ||||
msg = "Entry \x0302{0}\x0F renamed to \x0302{1}\x0F." | |||||
msg = "Entry \x0302{0}\x0f renamed to \x0302{1}\x0f." | |||||
self.reply(data, msg.format(data.args[1], newtitle)) | self.reply(data, msg.format(data.args[1], newtitle)) | ||||
def do_delete(self, data): | def do_delete(self, data): | ||||
@@ -286,7 +288,7 @@ class Notes(Command): | |||||
try: | try: | ||||
id_, author = conn.execute(query1, (slug,)).fetchone() | id_, author = conn.execute(query1, (slug,)).fetchone() | ||||
except (sqlite.OperationalError, TypeError): | except (sqlite.OperationalError, TypeError): | ||||
msg = "Entry \x0302{0}\x0F not found.".format(data.args[1]) | |||||
msg = f"Entry \x0302{data.args[1]}\x0f not found." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
permdb = self.config.irc["permissions"] | permdb = self.config.irc["permissions"] | ||||
@@ -297,7 +299,7 @@ class Notes(Command): | |||||
conn.execute(query2, (id_,)) | conn.execute(query2, (id_,)) | ||||
conn.execute(query3, (id_,)) | conn.execute(query3, (id_,)) | ||||
self.reply(data, "Entry \x0302{0}\x0F deleted.".format(data.args[1])) | |||||
self.reply(data, f"Entry \x0302{data.args[1]}\x0f deleted.") | |||||
def _slugify(self, name): | def _slugify(self, name): | ||||
"""Convert *name* into an identifier for storing in the database.""" | """Convert *name* into an identifier for storing in the database.""" | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,9 +20,11 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Quit(Command): | class Quit(Command): | ||||
"""Quit, restart, or reload components from the bot. Only the owners can | """Quit, restart, or reload components from the bot. Only the owners can | ||||
run this command.""" | run this command.""" | ||||
name = "quit" | name = "quit" | ||||
commands = ["quit", "restart", "reload"] | commands = ["quit", "restart", "reload"] | ||||
@@ -45,24 +45,26 @@ class Quit(Command): | |||||
reason = " ".join(args) | reason = " ".join(args) | ||||
else: | else: | ||||
if not args or args[0].lower() != data.my_nick: | if not args or args[0].lower() != data.my_nick: | ||||
self.reply(data, "To confirm this action, the first argument must be my name.") | |||||
self.reply( | |||||
data, "To confirm this action, the first argument must be my name." | |||||
) | |||||
return | return | ||||
reason = " ".join(args[1:]) | reason = " ".join(args[1:]) | ||||
if reason: | if reason: | ||||
self.bot.stop("Stopped by {0}: {1}".format(data.nick, reason)) | |||||
self.bot.stop(f"Stopped by {data.nick}: {reason}") | |||||
else: | else: | ||||
self.bot.stop("Stopped by {0}".format(data.nick)) | |||||
self.bot.stop(f"Stopped by {data.nick}") | |||||
def do_restart(self, data): | def do_restart(self, data): | ||||
if data.args: | if data.args: | ||||
msg = " ".join(data.args) | msg = " ".join(data.args) | ||||
self.bot.restart("Restarted by {0}: {1}".format(data.nick, msg)) | |||||
self.bot.restart(f"Restarted by {data.nick}: {msg}") | |||||
else: | else: | ||||
self.bot.restart("Restarted by {0}".format(data.nick)) | |||||
self.bot.restart(f"Restarted by {data.nick}") | |||||
def do_reload(self, data): | def do_reload(self, data): | ||||
self.logger.info("{0} requested command/task reload".format(data.nick)) | |||||
self.logger.info(f"{data.nick} requested command/task reload") | |||||
self.bot.commands.load() | self.bot.commands.load() | ||||
self.bot.tasks.load() | self.bot.tasks.load() | ||||
self.reply(data, "IRC commands and bot tasks reloaded.") | self.reply(data, "IRC commands and bot tasks reloaded.") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -26,8 +24,10 @@ from time import mktime | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Registration(Command): | class Registration(Command): | ||||
"""Return when a user registered.""" | """Return when a user registered.""" | ||||
name = "registration" | name = "registration" | ||||
commands = ["registration", "reg", "age"] | commands = ["registration", "reg", "age"] | ||||
@@ -35,7 +35,7 @@ class Registration(Command): | |||||
if not data.args: | if not data.args: | ||||
name = data.nick | name = data.nick | ||||
else: | else: | ||||
name = ' '.join(data.args) | |||||
name = " ".join(data.args) | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
user = site.get_user(name) | user = site.get_user(name) | ||||
@@ -43,7 +43,7 @@ class Registration(Command): | |||||
try: | try: | ||||
reg = user.registration | reg = user.registration | ||||
except exceptions.UserNotFoundError: | except exceptions.UserNotFoundError: | ||||
msg = "The user \x0302{0}\x0F does not exist." | |||||
msg = "The user \x0302{0}\x0f does not exist." | |||||
self.reply(data, msg.format(name)) | self.reply(data, msg.format(name)) | ||||
return | return | ||||
@@ -58,15 +58,16 @@ class Registration(Command): | |||||
else: | else: | ||||
gender = "They're" # Singular they? | gender = "They're" # Singular they? | ||||
msg = "\x0302{0}\x0F registered on {1}. {2} {3} old." | |||||
msg = "\x0302{0}\x0f registered on {1}. {2} {3} old." | |||||
self.reply(data, msg.format(name, date, gender, age)) | self.reply(data, msg.format(name, date, gender, age)) | ||||
def get_age(self, birth): | def get_age(self, birth): | ||||
msg = [] | msg = [] | ||||
def insert(unit, num): | def insert(unit, num): | ||||
if not num: | if not num: | ||||
return | return | ||||
msg.append("{0} {1}".format(num, unit if num == 1 else unit + "s")) | |||||
msg.append("{} {}".format(num, unit if num == 1 else unit + "s")) | |||||
now = datetime.utcnow() | now = datetime.utcnow() | ||||
bd_passed = now.timetuple()[1:-3] < birth.timetuple()[1:-3] | bd_passed = now.timetuple()[1:-3] < birth.timetuple()[1:-3] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,21 +19,21 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import ast | import ast | ||||
from itertools import chain | |||||
import operator | import operator | ||||
import random | import random | ||||
from threading import RLock, Thread | |||||
import time | import time | ||||
from itertools import chain | |||||
from threading import RLock, Thread | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
from earwigbot.irc import Data | from earwigbot.irc import Data | ||||
DISPLAY = ["display", "show", "info", "details"] | DISPLAY = ["display", "show", "info", "details"] | ||||
CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget", | |||||
"disregard"] | |||||
CANCEL = ["cancel", "stop", "delete", "del", "stop", "unremind", "forget", "disregard"] | |||||
SNOOZE = ["snooze", "delay", "reset", "adjust", "modify", "change"] | SNOOZE = ["snooze", "delay", "reset", "adjust", "modify", "change"] | ||||
SNOOZE_ONLY = ["snooze", "delay", "reset"] | SNOOZE_ONLY = ["snooze", "delay", "reset"] | ||||
def _format_time(epoch): | def _format_time(epoch): | ||||
"""Format a UNIX timestamp nicely.""" | """Format a UNIX timestamp nicely.""" | ||||
lctime = time.localtime(epoch) | lctime = time.localtime(epoch) | ||||
@@ -48,9 +46,17 @@ def _format_time(epoch): | |||||
class Remind(Command): | class Remind(Command): | ||||
"""Set a message to be repeated to you in a certain amount of time. See | """Set a message to be repeated to you in a certain amount of time. See | ||||
usage with !remind help.""" | usage with !remind help.""" | ||||
name = "remind" | name = "remind" | ||||
commands = ["remind", "reminder", "reminders", "snooze", "cancel", | |||||
"unremind", "forget"] | |||||
commands = [ | |||||
"remind", | |||||
"reminder", | |||||
"reminders", | |||||
"snooze", | |||||
"cancel", | |||||
"unremind", | |||||
"forget", | |||||
] | |||||
@staticmethod | @staticmethod | ||||
def _normalize(command): | def _normalize(command): | ||||
@@ -68,19 +74,27 @@ class Remind(Command): | |||||
def _parse_time(arg): | def _parse_time(arg): | ||||
"""Parse the wait time for a reminder.""" | """Parse the wait time for a reminder.""" | ||||
ast_to_op = { | ast_to_op = { | ||||
ast.Add: operator.add, ast.Sub: operator.sub, | |||||
ast.Mult: operator.mul, ast.Div: operator.truediv, | |||||
ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, | |||||
ast.Pow: operator.pow | |||||
ast.Add: operator.add, | |||||
ast.Sub: operator.sub, | |||||
ast.Mult: operator.mul, | |||||
ast.Div: operator.truediv, | |||||
ast.FloorDiv: operator.floordiv, | |||||
ast.Mod: operator.mod, | |||||
ast.Pow: operator.pow, | |||||
} | } | ||||
time_units = { | time_units = { | ||||
"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800, "y": 31536000 | |||||
"s": 1, | |||||
"m": 60, | |||||
"h": 3600, | |||||
"d": 86400, | |||||
"w": 604800, | |||||
"y": 31536000, | |||||
} | } | ||||
def _evaluate(node): | def _evaluate(node): | ||||
"""Convert an AST node into a real number or raise an exception.""" | """Convert an AST node into a real number or raise an exception.""" | ||||
if isinstance(node, ast.Num): | if isinstance(node, ast.Num): | ||||
if not isinstance(node.n, (int, float)): | |||||
if not isinstance(node.n, int | float): | |||||
raise ValueError(node.n) | raise ValueError(node.n) | ||||
return node.n | return node.n | ||||
elif isinstance(node, ast.BinOp): | elif isinstance(node, ast.BinOp): | ||||
@@ -114,7 +128,7 @@ class Remind(Command): | |||||
"""Get a free ID for a new reminder.""" | """Get a free ID for a new reminder.""" | ||||
taken = set(robj.id for robj in chain(*list(self.reminders.values()))) | taken = set(robj.id for robj in chain(*list(self.reminders.values()))) | ||||
num = random.choice(list(set(range(4096)) - taken)) | num = random.choice(list(set(range(4096)) - taken)) | ||||
return "R{0:03X}".format(num) | |||||
return f"R{num:03X}" | |||||
def _start_reminder(self, reminder, user): | def _start_reminder(self, reminder, user): | ||||
"""Start the given reminder object for the given user.""" | """Start the given reminder object for the given user.""" | ||||
@@ -129,12 +143,14 @@ class Remind(Command): | |||||
try: | try: | ||||
wait = self._parse_time(data.args[0]) | wait = self._parse_time(data.args[0]) | ||||
except ValueError: | except ValueError: | ||||
msg = "Invalid time \x02{0}\x0F. Time must be a positive integer, in seconds." | |||||
msg = ( | |||||
"Invalid time \x02{0}\x0f. Time must be a positive integer, in seconds." | |||||
) | |||||
return self.reply(data, msg.format(data.args[0])) | return self.reply(data, msg.format(data.args[0])) | ||||
if wait > 1000 * 365 * 24 * 60 * 60: | if wait > 1000 * 365 * 24 * 60 * 60: | ||||
# Hard to think of a good upper limit, but 1000 years works. | # Hard to think of a good upper limit, but 1000 years works. | ||||
msg = "Given time \x02{0}\x0F is too large. Keep it reasonable." | |||||
msg = "Given time \x02{0}\x0f is too large. Keep it reasonable." | |||||
return self.reply(data, msg.format(data.args[0])) | return self.reply(data, msg.format(data.args[0])) | ||||
message = " ".join(data.args[1:]) | message = " ".join(data.args[1:]) | ||||
@@ -146,14 +162,15 @@ class Remind(Command): | |||||
reminder = _Reminder(rid, data.host, wait, message, data, self) | reminder = _Reminder(rid, data.host, wait, message, data, self) | ||||
self._start_reminder(reminder, data.host) | self._start_reminder(reminder, data.host) | ||||
msg = "Set reminder \x0303{0}\x0F ({1})." | |||||
msg = "Set reminder \x0303{0}\x0f ({1})." | |||||
self.reply(data, msg.format(rid, reminder.end_time)) | self.reply(data, msg.format(rid, reminder.end_time)) | ||||
def _display_reminder(self, data, reminder): | def _display_reminder(self, data, reminder): | ||||
"""Display a particular reminder's information.""" | """Display a particular reminder's information.""" | ||||
msg = 'Reminder \x0303{0}\x0F: {1} seconds ({2}): "{3}".' | |||||
msg = msg.format(reminder.id, reminder.wait, reminder.end_time, | |||||
reminder.message) | |||||
msg = 'Reminder \x0303{0}\x0f: {1} seconds ({2}): "{3}".' | |||||
msg = msg.format( | |||||
reminder.id, reminder.wait, reminder.end_time, reminder.message | |||||
) | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def _cancel_reminder(self, data, reminder): | def _cancel_reminder(self, data, reminder): | ||||
@@ -163,7 +180,7 @@ class Remind(Command): | |||||
self.reminders[data.host].remove(reminder) | self.reminders[data.host].remove(reminder) | ||||
if not self.reminders[data.host]: | if not self.reminders[data.host]: | ||||
del self.reminders[data.host] | del self.reminders[data.host] | ||||
msg = "Reminder \x0303{0}\x0F canceled." | |||||
msg = "Reminder \x0303{0}\x0f canceled." | |||||
self.reply(data, msg.format(reminder.id)) | self.reply(data, msg.format(reminder.id)) | ||||
def _snooze_reminder(self, data, reminder, arg=None): | def _snooze_reminder(self, data, reminder, arg=None): | ||||
@@ -176,7 +193,7 @@ class Remind(Command): | |||||
reminder.reset(duration) | reminder.reset(duration) | ||||
end = _format_time(reminder.end) | end = _format_time(reminder.end) | ||||
msg = "Reminder \x0303{0}\x0F {1} until {2}." | |||||
msg = "Reminder \x0303{0}\x0f {1} until {2}." | |||||
self.reply(data, msg.format(reminder.id, verb, end)) | self.reply(data, msg.format(reminder.id, verb, end)) | ||||
def _load_reminders(self): | def _load_reminders(self): | ||||
@@ -202,39 +219,52 @@ class Remind(Command): | |||||
def _show_reminders(self, data): | def _show_reminders(self, data): | ||||
"""Show all of a user's current reminders.""" | """Show all of a user's current reminders.""" | ||||
if data.host not in self.reminders: | if data.host not in self.reminders: | ||||
self.reply(data, "You have no reminders. Set one with " | |||||
"\x0306!remind [time] [message]\x0F. See also: " | |||||
"\x0306!remind help\x0F.") | |||||
self.reply( | |||||
data, | |||||
"You have no reminders. Set one with " | |||||
"\x0306!remind [time] [message]\x0f. See also: " | |||||
"\x0306!remind help\x0f.", | |||||
) | |||||
return | return | ||||
shorten = lambda s: (s[:37] + "..." if len(s) > 40 else s) | |||||
dest = lambda data: ( | |||||
"privately" if data.is_private else "in {0}".format(data.chan)) | |||||
fmt = lambda robj: '\x0303{0}\x0F ("{1}" {2}, {3})'.format( | |||||
robj.id, shorten(robj.message), dest(robj.data), robj.end_time) | |||||
def shorten(s): | |||||
return s[:37] + "..." if len(s) > 40 else s | |||||
def dest(data): | |||||
return "privately" if data.is_private else f"in {data.chan}" | |||||
def fmt(robj): | |||||
return f'\x0303{robj.id}\x0f ("{shorten(robj.message)}" {dest(robj.data)}, {robj.end_time})' | |||||
rlist = ", ".join(fmt(robj) for robj in self.reminders[data.host]) | rlist = ", ".join(fmt(robj) for robj in self.reminders[data.host]) | ||||
self.reply(data, "Your reminders: {0}.".format(rlist)) | |||||
self.reply(data, f"Your reminders: {rlist}.") | |||||
def _show_all_reminders(self, data): | def _show_all_reminders(self, data): | ||||
"""Show all reminders to bot admins.""" | """Show all reminders to bot admins.""" | ||||
if not self.config.irc["permissions"].is_admin(data): | if not self.config.irc["permissions"].is_admin(data): | ||||
self.reply(data, "You must be a bot admin to view other users' " | |||||
"reminders. View your own with " | |||||
"\x0306!reminders\x0F.") | |||||
self.reply( | |||||
data, | |||||
"You must be a bot admin to view other users' " | |||||
"reminders. View your own with " | |||||
"\x0306!reminders\x0f.", | |||||
) | |||||
return | return | ||||
if not self.reminders: | if not self.reminders: | ||||
self.reply(data, "There are no active reminders.") | self.reply(data, "There are no active reminders.") | ||||
return | return | ||||
dest = lambda data: ( | |||||
"privately" if data.is_private else "in {0}".format(data.chan)) | |||||
fmt = lambda robj, user: '\x0303{0}\x0F (for {1} {2}, {3})'.format( | |||||
robj.id, user, dest(robj.data), robj.end_time) | |||||
def dest(data): | |||||
return "privately" if data.is_private else f"in {data.chan}" | |||||
rlist = (fmt(rem, user) for user, rems in self.reminders.items() | |||||
for rem in rems) | |||||
self.reply(data, "All reminders: {0}.".format(", ".join(rlist))) | |||||
def fmt(robj, user): | |||||
return ( | |||||
f"\x0303{robj.id}\x0f (for {user} {dest(robj.data)}, {robj.end_time})" | |||||
) | |||||
rlist = ( | |||||
fmt(rem, user) for user, rems in self.reminders.items() for rem in rems | |||||
) | |||||
self.reply(data, "All reminders: {}.".format(", ".join(rlist))) | |||||
def _show_help(self, data): | def _show_help(self, data): | ||||
"""Reply to the user with help for all major subcommands.""" | """Reply to the user with help for all major subcommands.""" | ||||
@@ -245,10 +275,10 @@ class Remind(Command): | |||||
("Cancel", "!remind cancel [id]"), | ("Cancel", "!remind cancel [id]"), | ||||
("Adjust", "!remind adjust [id] [time]"), | ("Adjust", "!remind adjust [id] [time]"), | ||||
("Restart", "!snooze [id] [time]"), | ("Restart", "!snooze [id] [time]"), | ||||
("Admin", "!remind all") | |||||
("Admin", "!remind all"), | |||||
] | ] | ||||
extra = "The \x0306[id]\x0F can be omitted if you have only one reminder." | |||||
joined = " ".join("{0}: \x0306{1}\x0F.".format(k, v) for k, v in parts) | |||||
extra = "The \x0306[id]\x0f can be omitted if you have only one reminder." | |||||
joined = " ".join(f"{k}: \x0306{v}\x0f." for k, v in parts) | |||||
self.reply(data, joined + " " + extra) | self.reply(data, joined + " " + extra) | ||||
def _dispatch_command(self, data, command, args): | def _dispatch_command(self, data, command, args): | ||||
@@ -259,7 +289,9 @@ class Remind(Command): | |||||
try: | try: | ||||
reminder = self._get_reminder_by_id(user, args[0]) | reminder = self._get_reminder_by_id(user, args[0]) | ||||
except IndexError: | except IndexError: | ||||
msg = "Couldn't find a reminder for \x0302{0}\x0F with ID \x0303{1}\x0F." | |||||
msg = ( | |||||
"Couldn't find a reminder for \x0302{0}\x0f with ID \x0303{1}\x0f." | |||||
) | |||||
self.reply(data, msg.format(user, args[0])) | self.reply(data, msg.format(user, args[0])) | ||||
return | return | ||||
args.pop(0) | args.pop(0) | ||||
@@ -292,7 +324,7 @@ class Remind(Command): | |||||
elif command in SNOOZE: | elif command in SNOOZE: | ||||
self._snooze_reminder(data, reminder, args[0] if args else None) | self._snooze_reminder(data, reminder, args[0] if args else None) | ||||
else: | else: | ||||
msg = "Unknown action \x02{0}\x0F for reminder \x0303{1}\x0F." | |||||
msg = "Unknown action \x02{0}\x0f for reminder \x0303{1}\x0f." | |||||
self.reply(data, msg.format(command, reminder.id)) | self.reply(data, msg.format(command, reminder.id)) | ||||
def _process(self, data): | def _process(self, data): | ||||
@@ -317,8 +349,7 @@ class Remind(Command): | |||||
return self._create_reminder(data) | return self._create_reminder(data) | ||||
if len(data.args) == 1: | if len(data.args) == 1: | ||||
return self._dispatch_command(data, "display", data.args) | return self._dispatch_command(data, "display", data.args) | ||||
self._dispatch_command( | |||||
data, data.args[1], [data.args[0]] + data.args[2:]) | |||||
self._dispatch_command(data, data.args[1], [data.args[0]] + data.args[2:]) | |||||
@property | @property | ||||
def lock(self): | def lock(self): | ||||
@@ -431,6 +462,7 @@ class _ReminderThread: | |||||
class _Reminder: | class _Reminder: | ||||
"""Represents a single reminder.""" | """Represents a single reminder.""" | ||||
def __init__(self, rid, user, wait, message, data, cmdobj, end=None): | def __init__(self, rid, user, wait, message, data, cmdobj, end=None): | ||||
self.id = rid | self.id = rid | ||||
self.wait = wait | self.wait = wait | ||||
@@ -476,7 +508,7 @@ class _Reminder: | |||||
"""Return a string representing the end time of a reminder.""" | """Return a string representing the end time of a reminder.""" | ||||
if self._expired or self.end < time.time(): | if self._expired or self.end < time.time(): | ||||
return "expired" | return "expired" | ||||
return "ends {0}".format(_format_time(self.end)) | |||||
return f"ends {_format_time(self.end)}" | |||||
@property | @property | ||||
def expired(self): | def expired(self): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -23,8 +21,10 @@ | |||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Rights(Command): | class Rights(Command): | ||||
"""Retrieve a list of rights for a given username.""" | """Retrieve a list of rights for a given username.""" | ||||
name = "rights" | name = "rights" | ||||
commands = ["rights", "groups", "permissions", "privileges"] | commands = ["rights", "groups", "permissions", "privileges"] | ||||
@@ -32,7 +32,7 @@ class Rights(Command): | |||||
if not data.args: | if not data.args: | ||||
name = data.nick | name = data.nick | ||||
else: | else: | ||||
name = ' '.join(data.args) | |||||
name = " ".join(data.args) | |||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
user = site.get_user(name) | user = site.get_user(name) | ||||
@@ -40,7 +40,7 @@ class Rights(Command): | |||||
try: | try: | ||||
rights = user.groups | rights = user.groups | ||||
except exceptions.UserNotFoundError: | except exceptions.UserNotFoundError: | ||||
msg = "The user \x0302{0}\x0F does not exist." | |||||
msg = "The user \x0302{0}\x0f does not exist." | |||||
self.reply(data, msg.format(name)) | self.reply(data, msg.format(name)) | ||||
return | return | ||||
@@ -48,5 +48,5 @@ class Rights(Command): | |||||
rights.remove("*") # Remove the '*' group given to everyone | rights.remove("*") # Remove the '*' group given to everyone | ||||
except ValueError: | except ValueError: | ||||
pass | pass | ||||
msg = "The rights for \x0302{0}\x0F are {1}." | |||||
self.reply(data, msg.format(name, ', '.join(rights))) | |||||
msg = "The rights for \x0302{0}\x0f are {1}." | |||||
self.reply(data, msg.format(name, ", ".join(rights))) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,18 +18,30 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from ast import literal_eval | |||||
import re | import re | ||||
from ast import literal_eval | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
from earwigbot.irc import RC | from earwigbot.irc import RC | ||||
class Stalk(Command): | class Stalk(Command): | ||||
"""Stalk a particular user (!stalk/!unstalk) or page (!watch/!unwatch) for | """Stalk a particular user (!stalk/!unstalk) or page (!watch/!unwatch) for | ||||
edits. Prefix regular expressions with "re:" (uses re.match).""" | edits. Prefix regular expressions with "re:" (uses re.match).""" | ||||
name = "stalk" | name = "stalk" | ||||
commands = ["stalk", "watch", "unstalk", "unwatch", "stalks", "watches", | |||||
"allstalks", "allwatches", "unstalkall", "unwatchall"] | |||||
commands = [ | |||||
"stalk", | |||||
"watch", | |||||
"unstalk", | |||||
"unwatch", | |||||
"stalks", | |||||
"watches", | |||||
"allstalks", | |||||
"allwatches", | |||||
"unstalkall", | |||||
"unwatchall", | |||||
] | |||||
hooks = ["msg", "rc"] | hooks = ["msg", "rc"] | ||||
MAX_STALKS_PER_USER = 5 | MAX_STALKS_PER_USER = 5 | ||||
@@ -58,20 +68,29 @@ class Stalk(Command): | |||||
if data.is_admin: | if data.is_admin: | ||||
self.reply(data, self._all_stalks()) | self.reply(data, self._all_stalks()) | ||||
else: | else: | ||||
self.reply(data, "You must be a bot admin to view all stalked " | |||||
"users or watched pages. View your own with " | |||||
"\x0306!stalks\x0F.") | |||||
self.reply( | |||||
data, | |||||
"You must be a bot admin to view all stalked " | |||||
"users or watched pages. View your own with " | |||||
"\x0306!stalks\x0f.", | |||||
) | |||||
return | return | ||||
if data.command.endswith("all"): | if data.command.endswith("all"): | ||||
if not data.is_admin: | if not data.is_admin: | ||||
self.reply(data, "You must be a bot admin to unstalk a user " | |||||
"or unwatch a page for all users.") | |||||
self.reply( | |||||
data, | |||||
"You must be a bot admin to unstalk a user " | |||||
"or unwatch a page for all users.", | |||||
) | |||||
return | return | ||||
if not data.args: | if not data.args: | ||||
self.reply(data, "You must give a user to unstalk or a page " | |||||
"to unwatch. View all active with " | |||||
"\x0306!allstalks\x0F.") | |||||
self.reply( | |||||
data, | |||||
"You must give a user to unstalk or a page " | |||||
"to unwatch. View all active with " | |||||
"\x0306!allstalks\x0f.", | |||||
) | |||||
return | return | ||||
if not data.args or data.command in ["stalks", "watches"]: | if not data.args or data.command in ["stalks", "watches"]: | ||||
@@ -98,9 +117,12 @@ class Stalk(Command): | |||||
if data.is_private: | if data.is_private: | ||||
stalkinfo = (data.nick, None, modifiers) | stalkinfo = (data.nick, None, modifiers) | ||||
elif not data.is_admin: | elif not data.is_admin: | ||||
self.reply(data, "You must be a bot admin to stalk users or " | |||||
"watch pages publicly. Retry this command in " | |||||
"a private message.") | |||||
self.reply( | |||||
data, | |||||
"You must be a bot admin to stalk users or " | |||||
"watch pages publicly. Retry this command in " | |||||
"a private message.", | |||||
) | |||||
return | return | ||||
else: | else: | ||||
stalkinfo = (data.nick, data.chan, modifiers) | stalkinfo = (data.nick, data.chan, modifiers) | ||||
@@ -120,6 +142,7 @@ class Stalk(Command): | |||||
def _process_rc(self, rc): | def _process_rc(self, rc): | ||||
"""Process a watcher event.""" | """Process a watcher event.""" | ||||
def _update_chans(items, flags): | def _update_chans(items, flags): | ||||
for item in items: | for item in items: | ||||
modifiers = item[2] if len(item) > 2 else {} | modifiers = item[2] if len(item) > 2 else {} | ||||
@@ -167,7 +190,7 @@ class Stalk(Command): | |||||
pretty = rc.prettify(color=chan not in nocolor) | pretty = rc.prettify(color=chan not in nocolor) | ||||
if users: | if users: | ||||
nicks = ", ".join(sorted(users)) | nicks = ", ".join(sorted(users)) | ||||
msg = "\x02{0}\x0F: {1}".format(nicks, pretty) | |||||
msg = f"\x02{nicks}\x0f: {pretty}" | |||||
else: | else: | ||||
msg = pretty | msg = pretty | ||||
if len(msg) > 400: | if len(msg) > 400: | ||||
@@ -199,8 +222,10 @@ class Stalk(Command): | |||||
if not data.is_admin: | if not data.is_admin: | ||||
nstalks = len(self._get_stalks_by_nick(data.nick, table)) | nstalks = len(self._get_stalks_by_nick(data.nick, table)) | ||||
if nstalks >= self.MAX_STALKS_PER_USER: | if nstalks >= self.MAX_STALKS_PER_USER: | ||||
msg = ("Already {0}ing {1} {2}s for you, which is the limit " | |||||
"for non-bot admins.") | |||||
msg = ( | |||||
"Already {0}ing {1} {2}s for you, which is the limit " | |||||
"for non-bot admins." | |||||
) | |||||
self.reply(data, msg.format(verb, nstalks, stalktype)) | self.reply(data, msg.format(verb, nstalks, stalktype)) | ||||
return | return | ||||
if stalkinfo[1] and not stalkinfo[1].startswith("##"): | if stalkinfo[1] and not stalkinfo[1].startswith("##"): | ||||
@@ -218,7 +243,7 @@ class Stalk(Command): | |||||
else: | else: | ||||
table[target] = [stalkinfo] | table[target] = [stalkinfo] | ||||
msg = "Now {0}ing {1} \x0302{2}\x0F. Remove with \x0306!un{0} {2}\x0F." | |||||
msg = "Now {0}ing {1} \x0302{2}\x0f. Remove with \x0306!un{0} {2}\x0f." | |||||
self.reply(data, msg.format(verb, stalktype, target)) | self.reply(data, msg.format(verb, stalktype, target)) | ||||
self._save_stalks() | self._save_stalks() | ||||
@@ -240,11 +265,15 @@ class Stalk(Command): | |||||
to_remove.append(info) | to_remove.append(info) | ||||
if not to_remove: | if not to_remove: | ||||
msg = ("I haven't been {0}ing that {1} for you in the first " | |||||
"place. View your active {2} with \x0306!{2}\x0F.") | |||||
msg = ( | |||||
"I haven't been {0}ing that {1} for you in the first " | |||||
"place. View your active {2} with \x0306!{2}\x0f." | |||||
) | |||||
if data.is_admin: | if data.is_admin: | ||||
msg += (" As a bot admin, you can clear all active {2} on " | |||||
"that {1} with \x0306!un{0}all {3}\x0F.") | |||||
msg += ( | |||||
" As a bot admin, you can clear all active {2} on " | |||||
"that {1} with \x0306!un{0}all {3}\x0f." | |||||
) | |||||
self.reply(data, msg.format(verb, stalktype, plural, target)) | self.reply(data, msg.format(verb, stalktype, plural, target)) | ||||
return | return | ||||
@@ -252,7 +281,7 @@ class Stalk(Command): | |||||
table[target].remove(info) | table[target].remove(info) | ||||
if not table[target]: | if not table[target]: | ||||
del table[target] | del table[target] | ||||
msg = "No longer {0}ing {1} \x0302{2}\x0F for you." | |||||
msg = "No longer {0}ing {1} \x0302{2}\x0f for you." | |||||
self.reply(data, msg.format(verb, stalktype, target)) | self.reply(data, msg.format(verb, stalktype, target)) | ||||
self._save_stalks() | self._save_stalks() | ||||
@@ -270,53 +299,63 @@ class Stalk(Command): | |||||
try: | try: | ||||
del table[target] | del table[target] | ||||
except KeyError: | except KeyError: | ||||
msg = ("I haven't been {0}ing that {1} for anyone in the first " | |||||
"place. View all active {2} with \x0306!all{2}\x0F.") | |||||
msg = ( | |||||
"I haven't been {0}ing that {1} for anyone in the first " | |||||
"place. View all active {2} with \x0306!all{2}\x0f." | |||||
) | |||||
self.reply(data, msg.format(verb, stalktype, plural)) | self.reply(data, msg.format(verb, stalktype, plural)) | ||||
else: | else: | ||||
msg = "No longer {0}ing {1} \x0302{2}\x0F for anyone." | |||||
msg = "No longer {0}ing {1} \x0302{2}\x0f for anyone." | |||||
self.reply(data, msg.format(verb, stalktype, target)) | self.reply(data, msg.format(verb, stalktype, target)) | ||||
self._save_stalks() | self._save_stalks() | ||||
def _current_stalks(self, nick): | def _current_stalks(self, nick): | ||||
"""Return the given user's current stalks.""" | """Return the given user's current stalks.""" | ||||
def _format_chans(chans): | def _format_chans(chans): | ||||
if None in chans: | if None in chans: | ||||
chans.remove(None) | chans.remove(None) | ||||
if not chans: | if not chans: | ||||
return "privately" | return "privately" | ||||
if len(chans) == 1: | if len(chans) == 1: | ||||
return "in {0} and privately".format(chans[0]) | |||||
return f"in {chans[0]} and privately" | |||||
return "in " + ", ".join(chans) + ", and privately" | return "in " + ", ".join(chans) + ", and privately" | ||||
return "in " + ", ".join(chans) | return "in " + ", ".join(chans) | ||||
def _format_stalks(stalks): | def _format_stalks(stalks): | ||||
return ", ".join( | return ", ".join( | ||||
"\x0302{0}\x0F ({1})".format(target, _format_chans(chans)) | |||||
for target, chans in stalks.items()) | |||||
f"\x0302{target}\x0f ({_format_chans(chans)})" | |||||
for target, chans in stalks.items() | |||||
) | |||||
users = self._get_stalks_by_nick(nick, self._users) | users = self._get_stalks_by_nick(nick, self._users) | ||||
pages = self._get_stalks_by_nick(nick, self._pages) | pages = self._get_stalks_by_nick(nick, self._pages) | ||||
if users: | if users: | ||||
uinfo = " Users: {0}.".format(_format_stalks(users)) | |||||
uinfo = f" Users: {_format_stalks(users)}." | |||||
if pages: | if pages: | ||||
pinfo = " Pages: {0}.".format(_format_stalks(pages)) | |||||
pinfo = f" Pages: {_format_stalks(pages)}." | |||||
msg = "Currently stalking {0} user{1} and watching {2} page{3} for you.{4}{5}" | msg = "Currently stalking {0} user{1} and watching {2} page{3} for you.{4}{5}" | ||||
return msg.format(len(users), "s" if len(users) != 1 else "", | |||||
len(pages), "s" if len(pages) != 1 else "", | |||||
uinfo if users else "", pinfo if pages else "") | |||||
return msg.format( | |||||
len(users), | |||||
"s" if len(users) != 1 else "", | |||||
len(pages), | |||||
"s" if len(pages) != 1 else "", | |||||
uinfo if users else "", | |||||
pinfo if pages else "", | |||||
) | |||||
def _all_stalks(self): | def _all_stalks(self): | ||||
"""Return all existing stalks, for bot admins.""" | """Return all existing stalks, for bot admins.""" | ||||
def _format_info(info): | def _format_info(info): | ||||
if info[1]: | if info[1]: | ||||
result = "for {0} in {1}".format(info[0], info[1]) | |||||
result = f"for {info[0]} in {info[1]}" | |||||
else: | else: | ||||
result = "for {0} privately".format(info[0]) | |||||
result = f"for {info[0]} privately" | |||||
modifiers = ", ".join(info[2]) if len(info) > 2 else "" | modifiers = ", ".join(info[2]) if len(info) > 2 else "" | ||||
if modifiers: | if modifiers: | ||||
result += " ({0})".format(modifiers) | |||||
result += f" ({modifiers})" | |||||
return result | return result | ||||
def _format_data(data): | def _format_data(data): | ||||
@@ -324,19 +363,25 @@ class Stalk(Command): | |||||
def _format_stalks(stalks): | def _format_stalks(stalks): | ||||
return ", ".join( | return ", ".join( | ||||
"\x0302{0}\x0F ({1})".format(target, _format_data(data)) | |||||
for target, data in stalks.items()) | |||||
f"\x0302{target}\x0f ({_format_data(data)})" | |||||
for target, data in stalks.items() | |||||
) | |||||
users, pages = self._users, self._pages | users, pages = self._users, self._pages | ||||
if users: | if users: | ||||
uinfo = " Users: {0}.".format(_format_stalks(users)) | |||||
uinfo = f" Users: {_format_stalks(users)}." | |||||
if pages: | if pages: | ||||
pinfo = " Pages: {0}.".format(_format_stalks(pages)) | |||||
pinfo = f" Pages: {_format_stalks(pages)}." | |||||
msg = "Currently stalking {0} user{1} and watching {2} page{3}.{4}{5}" | msg = "Currently stalking {0} user{1} and watching {2} page{3}.{4}{5}" | ||||
return msg.format(len(users), "s" if len(users) != 1 else "", | |||||
len(pages), "s" if len(pages) != 1 else "", | |||||
uinfo if users else "", pinfo if pages else "") | |||||
return msg.format( | |||||
len(users), | |||||
"s" if len(users) != 1 else "", | |||||
len(pages), | |||||
"s" if len(pages) != 1 else "", | |||||
uinfo if users else "", | |||||
pinfo if pages else "", | |||||
) | |||||
def _load_stalks(self): | def _load_stalks(self): | ||||
"""Load saved stalks from the database.""" | """Load saved stalks from the database.""" | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,14 +22,16 @@ import random | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Test(Command): | class Test(Command): | ||||
"""Test the bot!""" | """Test the bot!""" | ||||
name = "test" | name = "test" | ||||
def process(self, data): | def process(self, data): | ||||
user = "\x02" + data.nick + "\x0F" # Wrap nick in bold | |||||
user = "\x02" + data.nick + "\x0f" # Wrap nick in bold | |||||
hey = random.randint(0, 1) | hey = random.randint(0, 1) | ||||
if hey: | if hey: | ||||
self.say(data.chan, "Hey {0}!".format(user)) | |||||
self.say(data.chan, f"Hey {user}!") | |||||
else: | else: | ||||
self.say(data.chan, "'Sup {0}?".format(user)) | |||||
self.say(data.chan, f"'Sup {user}?") |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,13 +18,15 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
import threading | |||||
import re | import re | ||||
import threading | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Threads(Command): | class Threads(Command): | ||||
"""Manage wiki tasks from IRC, and check on thread status.""" | """Manage wiki tasks from IRC, and check on thread status.""" | ||||
name = "threads" | name = "threads" | ||||
commands = ["tasks", "task", "threads", "tasklist"] | commands = ["tasks", "task", "threads", "tasklist"] | ||||
@@ -55,7 +55,7 @@ class Threads(Command): | |||||
self.do_listall() | self.do_listall() | ||||
else: # They asked us to do something we don't know | else: # They asked us to do something we don't know | ||||
msg = "Unknown argument: \x0303{0}\x0F.".format(data.args[0]) | |||||
msg = f"Unknown argument: \x0303{data.args[0]}\x0f." | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def do_list(self): | def do_list(self): | ||||
@@ -71,34 +71,38 @@ class Threads(Command): | |||||
tname = thread.name | tname = thread.name | ||||
ident = thread.ident % 10000 | ident = thread.ident % 10000 | ||||
if tname == "MainThread": | if tname == "MainThread": | ||||
t = "\x0302main\x0F (id {0})" | |||||
t = "\x0302main\x0f (id {0})" | |||||
normal_threads.append(t.format(ident)) | normal_threads.append(t.format(ident)) | ||||
elif tname in self.config.components: | elif tname in self.config.components: | ||||
t = "\x0302{0}\x0F (id {1})" | |||||
t = "\x0302{0}\x0f (id {1})" | |||||
normal_threads.append(t.format(tname, ident)) | normal_threads.append(t.format(tname, ident)) | ||||
elif tname.startswith("cvworker-"): | elif tname.startswith("cvworker-"): | ||||
t = "\x0302copyvio worker\x0F (site {0})" | |||||
daemon_threads.append(t.format(tname[len("cvworker-"):])) | |||||
t = "\x0302copyvio worker\x0f (site {0})" | |||||
daemon_threads.append(t.format(tname[len("cvworker-") :])) | |||||
else: | else: | ||||
match = re.findall(r"^(.*?) \((.*?)\)$", tname) | match = re.findall(r"^(.*?) \((.*?)\)$", tname) | ||||
if match: | if match: | ||||
t = "\x0302{0}\x0F (id {1}, since {2})" | |||||
t = "\x0302{0}\x0f (id {1}, since {2})" | |||||
thread_info = t.format(match[0][0], ident, match[0][1]) | thread_info = t.format(match[0][0], ident, match[0][1]) | ||||
daemon_threads.append(thread_info) | daemon_threads.append(thread_info) | ||||
else: | else: | ||||
t = "\x0302{0}\x0F (id {1})" | |||||
t = "\x0302{0}\x0f (id {1})" | |||||
daemon_threads.append(t.format(tname, ident)) | daemon_threads.append(t.format(tname, ident)) | ||||
if daemon_threads: | if daemon_threads: | ||||
if len(daemon_threads) > 1: | if len(daemon_threads) > 1: | ||||
msg = "\x02{0}\x0F threads active: {1}, and \x02{2}\x0F command/task threads: {3}." | |||||
msg = "\x02{0}\x0f threads active: {1}, and \x02{2}\x0f command/task threads: {3}." | |||||
else: | else: | ||||
msg = "\x02{0}\x0F threads active: {1}, and \x02{2}\x0F command/task thread: {3}." | |||||
msg = msg.format(len(threads), ', '.join(normal_threads), | |||||
len(daemon_threads), ', '.join(daemon_threads)) | |||||
msg = "\x02{0}\x0f threads active: {1}, and \x02{2}\x0f command/task thread: {3}." | |||||
msg = msg.format( | |||||
len(threads), | |||||
", ".join(normal_threads), | |||||
len(daemon_threads), | |||||
", ".join(daemon_threads), | |||||
) | |||||
else: | else: | ||||
msg = "\x02{0}\x0F threads active: {1}, and \x020\x0F command/task threads." | |||||
msg = msg.format(len(threads), ', '.join(normal_threads)) | |||||
msg = "\x02{0}\x0f threads active: {1}, and \x020\x0f command/task threads." | |||||
msg = msg.format(len(threads), ", ".join(normal_threads)) | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
@@ -111,17 +115,17 @@ class Threads(Command): | |||||
threadlist = [t for t in threads if t.name.startswith(task)] | threadlist = [t for t in threads if t.name.startswith(task)] | ||||
ids = [str(t.ident) for t in threadlist] | ids = [str(t.ident) for t in threadlist] | ||||
if not ids: | if not ids: | ||||
tasklist.append("\x0302{0}\x0F (idle)".format(task)) | |||||
tasklist.append(f"\x0302{task}\x0f (idle)") | |||||
elif len(ids) == 1: | elif len(ids) == 1: | ||||
t = "\x0302{0}\x0F (\x02active\x0F as id {1})" | |||||
t = "\x0302{0}\x0f (\x02active\x0f as id {1})" | |||||
tasklist.append(t.format(task, ids[0])) | tasklist.append(t.format(task, ids[0])) | ||||
else: | else: | ||||
t = "\x0302{0}\x0F (\x02active\x0F as ids {1})" | |||||
tasklist.append(t.format(task, ', '.join(ids))) | |||||
t = "\x0302{0}\x0f (\x02active\x0f as ids {1})" | |||||
tasklist.append(t.format(task, ", ".join(ids))) | |||||
tasks = ", ".join(tasklist) | tasks = ", ".join(tasklist) | ||||
msg = "\x02{0}\x0F tasks loaded: {1}.".format(len(tasklist), tasks) | |||||
msg = f"\x02{len(tasklist)}\x0f tasks loaded: {tasks}." | |||||
self.reply(self.data, msg) | self.reply(self.data, msg) | ||||
def do_start(self): | def do_start(self): | ||||
@@ -143,8 +147,9 @@ class Threads(Command): | |||||
data.kwargs["fromIRC"] = True | data.kwargs["fromIRC"] = True | ||||
data.kwargs["_IRCCallback"] = lambda: self.reply( | data.kwargs["_IRCCallback"] = lambda: self.reply( | ||||
data, "Task \x0302{0}\x0F finished.".format(task_name)) | |||||
data, f"Task \x0302{task_name}\x0f finished." | |||||
) | |||||
self.bot.tasks.start(task_name, **data.kwargs) | self.bot.tasks.start(task_name, **data.kwargs) | ||||
msg = "Task \x0302{0}\x0F started.".format(task_name) | |||||
msg = f"Task \x0302{task_name}\x0f started." | |||||
self.reply(data, msg) | self.reply(data, msg) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -29,9 +27,11 @@ from earwigbot.commands import Command | |||||
pytz = importer.new("pytz") | pytz = importer.new("pytz") | ||||
class Time(Command): | class Time(Command): | ||||
"""Report the current time in any timezone (UTC default), UNIX epoch time, | """Report the current time in any timezone (UTC default), UNIX epoch time, | ||||
or beat time.""" | or beat time.""" | ||||
name = "time" | name = "time" | ||||
commands = ["time", "beats", "swatch", "epoch", "date"] | commands = ["time", "beats", "swatch", "epoch", "date"] | ||||
@@ -54,7 +54,7 @@ class Time(Command): | |||||
def do_beats(self, data): | def do_beats(self, data): | ||||
beats = ((time() + 3600) % 86400) / 86.4 | beats = ((time() + 3600) % 86400) / 86.4 | ||||
beats = int(floor(beats)) | beats = int(floor(beats)) | ||||
self.reply(data, "@{0:0>3}".format(beats)) | |||||
self.reply(data, f"@{beats:0>3}") | |||||
def do_time(self, data, timezone): | def do_time(self, data, timezone): | ||||
try: | try: | ||||
@@ -64,7 +64,7 @@ class Time(Command): | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
except pytz.exceptions.UnknownTimeZoneError: | except pytz.exceptions.UnknownTimeZoneError: | ||||
self.reply(data, "Unknown timezone: {0}.".format(timezone)) | |||||
self.reply(data, f"Unknown timezone: {timezone}.") | |||||
return | return | ||||
now = pytz.utc.localize(datetime.utcnow()).astimezone(tzinfo) | now = pytz.utc.localize(datetime.utcnow()).astimezone(tzinfo) | ||||
self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S %Z")) | self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S %Z")) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,8 +22,10 @@ from unicodedata import normalize | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Trout(Command): | class Trout(Command): | ||||
"""Slap someone with a trout, or related fish.""" | """Slap someone with a trout, or related fish.""" | ||||
name = "trout" | name = "trout" | ||||
commands = ["trout", "whale"] | commands = ["trout", "whale"] | ||||
@@ -44,5 +44,5 @@ class Trout(Command): | |||||
if normal in self.exceptions: | if normal in self.exceptions: | ||||
self.reply(data, self.exceptions[normal]) | self.reply(data, self.exceptions[normal]) | ||||
else: | else: | ||||
msg = "slaps \x02{0}\x0F around a bit with a large {1}." | |||||
msg = "slaps \x02{0}\x0f around a bit with a large {1}." | |||||
self.action(data.chan, msg.format(target, animal)) | self.action(data.chan, msg.format(target, animal)) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,8 +20,10 @@ | |||||
from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
class Watchers(Command): | class Watchers(Command): | ||||
"""Get the number of users watching a given page.""" | """Get the number of users watching a given page.""" | ||||
name = "watchers" | name = "watchers" | ||||
def process(self, data): | def process(self, data): | ||||
@@ -33,13 +33,14 @@ class Watchers(Command): | |||||
return | return | ||||
site = self.bot.wiki.get_site() | site = self.bot.wiki.get_site() | ||||
query = site.api_query(action="query", prop="info", inprop="watchers", | |||||
titles=" ".join(data.args)) | |||||
query = site.api_query( | |||||
action="query", prop="info", inprop="watchers", titles=" ".join(data.args) | |||||
) | |||||
page = list(query["query"]["pages"].values())[0] | page = list(query["query"]["pages"].values())[0] | ||||
title = page["title"].encode("utf8") | title = page["title"].encode("utf8") | ||||
if "invalid" in page: | if "invalid" in page: | ||||
msg = "\x0302{0}\x0F is an invalid page title." | |||||
msg = "\x0302{0}\x0f is an invalid page title." | |||||
self.reply(data, msg.format(title)) | self.reply(data, msg.format(title)) | ||||
return | return | ||||
@@ -48,5 +49,5 @@ class Watchers(Command): | |||||
else: | else: | ||||
watchers = "<30" | watchers = "<30" | ||||
plural = "" if watchers == 1 else "s" | plural = "" if watchers == 1 else "s" | ||||
msg = "\x0302{0}\x0F has \x02{1}\x0F watcher{2}." | |||||
msg = "\x0302{0}\x0f has \x02{1}\x0f watcher{2}." | |||||
self.reply(data, msg.format(title, watchers, plural)) | self.reply(data, msg.format(title, watchers, plural)) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,12 +19,12 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import base64 | import base64 | ||||
from collections import OrderedDict | |||||
from getpass import getpass | |||||
import logging | import logging | ||||
import logging.handlers | import logging.handlers | ||||
from os import mkdir, path | |||||
import stat | import stat | ||||
from collections import OrderedDict | |||||
from getpass import getpass | |||||
from os import mkdir, path | |||||
import yaml | import yaml | ||||
@@ -44,6 +42,7 @@ pbkdf2 = importer.new("cryptography.hazmat.primitives.kdf.pbkdf2") | |||||
__all__ = ["BotConfig"] | __all__ = ["BotConfig"] | ||||
class BotConfig: | class BotConfig: | ||||
""" | """ | ||||
**EarwigBot: YAML Config File Manager** | **EarwigBot: YAML Config File Manager** | ||||
@@ -89,8 +88,14 @@ class BotConfig: | |||||
self._tasks = ConfigNode() | self._tasks = ConfigNode() | ||||
self._metadata = ConfigNode() | self._metadata = ConfigNode() | ||||
self._nodes = [self._components, self._wiki, self._irc, self._commands, | |||||
self._tasks, self._metadata] | |||||
self._nodes = [ | |||||
self._components, | |||||
self._wiki, | |||||
self._irc, | |||||
self._commands, | |||||
self._tasks, | |||||
self._metadata, | |||||
] | |||||
self._decryptable_nodes = [ # Default nodes to decrypt | self._decryptable_nodes = [ # Default nodes to decrypt | ||||
(self._wiki, ("password",)), | (self._wiki, ("password",)), | ||||
@@ -107,7 +112,7 @@ class BotConfig: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the BotConfig.""" | """Return a nice string representation of the BotConfig.""" | ||||
return "<BotConfig at {0}>".format(self.root_dir) | |||||
return f"<BotConfig at {self.root_dir}>" | |||||
def _handle_missing_config(self): | def _handle_missing_config(self): | ||||
print("Config file missing or empty:", self._config_path) | print("Config file missing or empty:", self._config_path) | ||||
@@ -124,11 +129,11 @@ class BotConfig: | |||||
def _load(self): | def _load(self): | ||||
"""Load data from our JSON config file (config.yml) into self._data.""" | """Load data from our JSON config file (config.yml) into self._data.""" | ||||
filename = self._config_path | filename = self._config_path | ||||
with open(filename, 'r') as fp: | |||||
with open(filename) as fp: | |||||
try: | try: | ||||
self._data = yaml.load(fp, OrderedLoader) | self._data = yaml.load(fp, OrderedLoader) | ||||
except yaml.YAMLError: | except yaml.YAMLError: | ||||
print("Error parsing config file {0}:".format(filename)) | |||||
print(f"Error parsing config file {filename}:") | |||||
raise | raise | ||||
def _setup_logging(self): | def _setup_logging(self): | ||||
@@ -142,11 +147,13 @@ class BotConfig: | |||||
if self.metadata.get("enableLogging"): | if self.metadata.get("enableLogging"): | ||||
hand = logging.handlers.TimedRotatingFileHandler | hand = logging.handlers.TimedRotatingFileHandler | ||||
logfile = lambda f: path.join(log_dir, f) | |||||
def logfile(f): | |||||
return path.join(log_dir, f) | |||||
if not path.isdir(log_dir): | if not path.isdir(log_dir): | ||||
if not path.exists(log_dir): | if not path.exists(log_dir): | ||||
mkdir(log_dir, stat.S_IWUSR|stat.S_IRUSR|stat.S_IXUSR) | |||||
mkdir(log_dir, stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR) | |||||
else: | else: | ||||
msg = "log_dir ({0}) exists but is not a directory!" | msg = "log_dir ({0}) exists but is not a directory!" | ||||
print(msg.format(log_dir)) | print(msg.format(log_dir)) | ||||
@@ -296,7 +303,8 @@ class BotConfig: | |||||
raise NoConfigError(e) | raise NoConfigError(e) | ||||
key = getpass("Enter key to decrypt bot passwords: ") | key = getpass("Enter key to decrypt bot passwords: ") | ||||
self._decryption_cipher = fernet.Fernet( | self._decryption_cipher = fernet.Fernet( | ||||
base64.urlsafe_b64encode(kdf.derive(key.encode()))) | |||||
base64.urlsafe_b64encode(kdf.derive(key.encode())) | |||||
) | |||||
for node, nodes in self._decryptable_nodes: | for node, nodes in self._decryptable_nodes: | ||||
self._decrypt(node, nodes) | self._decrypt(node, nodes) | ||||
@@ -336,8 +344,13 @@ class BotConfig: | |||||
# or just the task_name: | # or just the task_name: | ||||
tasks = [] | tasks = [] | ||||
now = {"minute": minute, "hour": hour, "month_day": month_day, | |||||
"month": month, "week_day": week_day} | |||||
now = { | |||||
"minute": minute, | |||||
"hour": hour, | |||||
"month_day": month_day, | |||||
"month": month, | |||||
"week_day": week_day, | |||||
} | |||||
data = self._data.get("schedule", []) | data = self._data.get("schedule", []) | ||||
for event in data: | for event in data: | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,12 +22,13 @@ import logging | |||||
__all__ = ["BotFormatter"] | __all__ = ["BotFormatter"] | ||||
class BotFormatter(logging.Formatter): | class BotFormatter(logging.Formatter): | ||||
def __init__(self, color=False): | def __init__(self, color=False): | ||||
self._format = super().format | self._format = super().format | ||||
if color: | if color: | ||||
fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s" | fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s" | ||||
self.format = lambda rec: self._format(self.format_color(rec)) | |||||
self.format = lambda record: self._format(self.format_color(record)) | |||||
else: | else: | ||||
fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s" | fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s" | ||||
self.format = self._format | self.format = self._format | ||||
@@ -37,15 +36,15 @@ class BotFormatter(logging.Formatter): | |||||
super().__init__(fmt=fmt, datefmt=datefmt) | super().__init__(fmt=fmt, datefmt=datefmt) | ||||
def format_color(self, record): | def format_color(self, record): | ||||
l = record.levelname.ljust(8) | |||||
lvl = record.levelname.ljust(8) | |||||
if record.levelno == logging.DEBUG: | if record.levelno == logging.DEBUG: | ||||
record.lvl = l.join(("\x1b[34m", "\x1b[0m")) # Blue | |||||
record.lvl = lvl.join(("\x1b[34m", "\x1b[0m")) # Blue | |||||
if record.levelno == logging.INFO: | if record.levelno == logging.INFO: | ||||
record.lvl = l.join(("\x1b[32m", "\x1b[0m")) # Green | |||||
record.lvl = lvl.join(("\x1b[32m", "\x1b[0m")) # Green | |||||
if record.levelno == logging.WARNING: | if record.levelno == logging.WARNING: | ||||
record.lvl = l.join(("\x1b[33m", "\x1b[0m")) # Yellow | |||||
record.lvl = lvl.join(("\x1b[33m", "\x1b[0m")) # Yellow | |||||
if record.levelno == logging.ERROR: | if record.levelno == logging.ERROR: | ||||
record.lvl = l.join(("\x1b[31m", "\x1b[0m")) # Red | |||||
record.lvl = lvl.join(("\x1b[31m", "\x1b[0m")) # Red | |||||
if record.levelno == logging.CRITICAL: | if record.levelno == logging.CRITICAL: | ||||
record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | |||||
record.lvl = lvl.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | |||||
return record | return record |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,6 +23,7 @@ from collections import OrderedDict | |||||
__all__ = ["ConfigNode"] | __all__ = ["ConfigNode"] | ||||
class ConfigNode: | class ConfigNode: | ||||
def __init__(self): | def __init__(self): | ||||
self._data = OrderedDict() | self._data = OrderedDict() | ||||
@@ -56,8 +55,7 @@ class ConfigNode: | |||||
self._data[key] = item | self._data[key] = item | ||||
def __iter__(self): | def __iter__(self): | ||||
for key in self._data: | |||||
yield key | |||||
yield from self._data | |||||
def __contains__(self, item): | def __contains__(self, item): | ||||
return item in self._data | return item in self._data | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -35,6 +33,7 @@ import yaml | |||||
__all__ = ["OrderedLoader", "OrderedDumper"] | __all__ = ["OrderedLoader", "OrderedDumper"] | ||||
class OrderedLoader(yaml.Loader): | class OrderedLoader(yaml.Loader): | ||||
"""A YAML loader that loads mappings into ordered dictionaries.""" | """A YAML loader that loads mappings into ordered dictionaries.""" | ||||
@@ -54,9 +53,12 @@ class OrderedLoader(yaml.Loader): | |||||
if isinstance(node, yaml.MappingNode): | if isinstance(node, yaml.MappingNode): | ||||
self.flatten_mapping(node) | self.flatten_mapping(node) | ||||
else: | else: | ||||
raise yaml.constructor.ConstructorError(None, None, | |||||
"expected a mapping node, but found {0}".format(node.id), | |||||
node.start_mark) | |||||
raise yaml.constructor.ConstructorError( | |||||
None, | |||||
None, | |||||
f"expected a mapping node, but found {node.id}", | |||||
node.start_mark, | |||||
) | |||||
mapping = OrderedDict() | mapping = OrderedDict() | ||||
for key_node, value_node in node.value: | for key_node, value_node in node.value: | ||||
@@ -65,9 +67,11 @@ class OrderedLoader(yaml.Loader): | |||||
hash(key) | hash(key) | ||||
except TypeError as exc: | except TypeError as exc: | ||||
raise yaml.constructor.ConstructorError( | raise yaml.constructor.ConstructorError( | ||||
"while constructing a mapping", node.start_mark, | |||||
"found unacceptable key ({0})".format(exc), | |||||
key_node.start_mark) | |||||
"while constructing a mapping", | |||||
node.start_mark, | |||||
f"found unacceptable key ({exc})", | |||||
key_node.start_mark, | |||||
) | |||||
value = self.construct_object(value_node, deep=deep) | value = self.construct_object(value_node, deep=deep) | ||||
mapping[key] = value | mapping[key] = value | ||||
return mapping | return mapping | ||||
@@ -91,11 +95,9 @@ class OrderedDumper(yaml.SafeDumper): | |||||
for item_key, item_value in mapping: | for item_key, item_value in mapping: | ||||
node_key = self.represent_data(item_key) | node_key = self.represent_data(item_key) | ||||
node_value = self.represent_data(item_value) | node_value = self.represent_data(item_value) | ||||
if not (isinstance(node_key, yaml.ScalarNode) and not | |||||
node_key.style): | |||||
if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): | |||||
best_style = False | best_style = False | ||||
if not (isinstance(node_value, yaml.ScalarNode) and not | |||||
node_value.style): | |||||
if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): | |||||
best_style = False | best_style = False | ||||
value.append((node_key, node_value)) | value.append((node_key, node_value)) | ||||
if flow_style is None: | if flow_style is None: | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,12 +18,13 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from fnmatch import fnmatch | |||||
import sqlite3 as sqlite | import sqlite3 as sqlite | ||||
from fnmatch import fnmatch | |||||
from threading import Lock | from threading import Lock | ||||
__all__ = ["PermissionsDB"] | __all__ = ["PermissionsDB"] | ||||
class PermissionsDB: | class PermissionsDB: | ||||
""" | """ | ||||
**EarwigBot: Permissions Database Manager** | **EarwigBot: Permissions Database Manager** | ||||
@@ -33,6 +32,7 @@ class PermissionsDB: | |||||
Controls the :file:`permissions.db` file, which stores the bot's owners and | Controls the :file:`permissions.db` file, which stores the bot's owners and | ||||
admins for the purposes of using certain dangerous IRC commands. | admins for the purposes of using certain dangerous IRC commands. | ||||
""" | """ | ||||
ADMIN = 1 | ADMIN = 1 | ||||
OWNER = 2 | OWNER = 2 | ||||
@@ -49,7 +49,7 @@ class PermissionsDB: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the PermissionsDB.""" | """Return a nice string representation of the PermissionsDB.""" | ||||
return "<PermissionsDB at {0}>".format(self._dbfile) | |||||
return f"<PermissionsDB at {self._dbfile}>" | |||||
def _create(self, conn): | def _create(self, conn): | ||||
"""Initialize the permissions database with its necessary tables.""" | """Initialize the permissions database with its necessary tables.""" | ||||
@@ -198,8 +198,10 @@ class PermissionsDB: | |||||
with self._db_access_lock, sqlite.connect(self._dbfile) as conn: | with self._db_access_lock, sqlite.connect(self._dbfile) as conn: | ||||
conn.execute(query, (user, key)) | conn.execute(query, (user, key)) | ||||
class User: | class User: | ||||
"""A class that represents an IRC user for the purpose of testing rules.""" | """A class that represents an IRC user for the purpose of testing rules.""" | ||||
def __init__(self, nick, ident, host): | def __init__(self, nick, ident, host): | ||||
self.nick = nick | self.nick = nick | ||||
self.ident = ident | self.ident = ident | ||||
@@ -212,7 +214,7 @@ class User: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the User.""" | """Return a nice string representation of the User.""" | ||||
return "{0}!{1}@{2}".format(self.nick, self.ident, self.host) | |||||
return f"{self.nick}!{self.ident}@{self.host}" | |||||
def __contains__(self, user): | def __contains__(self, user): | ||||
if fnmatch(user.nick, self.nick): | if fnmatch(user.nick, self.nick): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,13 +19,13 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import base64 | import base64 | ||||
from collections import OrderedDict | |||||
from getpass import getpass | |||||
import os | import os | ||||
from os import chmod, makedirs, mkdir, path | |||||
import re | import re | ||||
import stat | import stat | ||||
import sys | import sys | ||||
from collections import OrderedDict | |||||
from getpass import getpass | |||||
from os import chmod, makedirs, mkdir, path | |||||
from textwrap import fill, wrap | from textwrap import fill, wrap | ||||
import yaml | import yaml | ||||
@@ -50,23 +48,27 @@ def process(bot, rc): | |||||
pass | pass | ||||
""" | """ | ||||
class ConfigScript: | class ConfigScript: | ||||
"""A script to guide a user through the creation of a new config file.""" | """A script to guide a user through the creation of a new config file.""" | ||||
WIDTH = 79 | WIDTH = 79 | ||||
PROMPT = "\x1b[32m> \x1b[0m" | PROMPT = "\x1b[32m> \x1b[0m" | ||||
PBKDF_ROUNDS = 100000 | PBKDF_ROUNDS = 100000 | ||||
def __init__(self, config): | def __init__(self, config): | ||||
self.config = config | self.config = config | ||||
self.data = OrderedDict([ | |||||
("metadata", OrderedDict()), | |||||
("components", OrderedDict()), | |||||
("wiki", OrderedDict()), | |||||
("irc", OrderedDict()), | |||||
("commands", OrderedDict()), | |||||
("tasks", OrderedDict()), | |||||
("schedule", []) | |||||
]) | |||||
self.data = OrderedDict( | |||||
[ | |||||
("metadata", OrderedDict()), | |||||
("components", OrderedDict()), | |||||
("wiki", OrderedDict()), | |||||
("irc", OrderedDict()), | |||||
("commands", OrderedDict()), | |||||
("tasks", OrderedDict()), | |||||
("schedule", []), | |||||
] | |||||
) | |||||
self._cipher = None | self._cipher = None | ||||
self._wmf = False | self._wmf = False | ||||
@@ -86,7 +88,7 @@ class ConfigScript: | |||||
def _ask(self, text, default=None, require=True): | def _ask(self, text, default=None, require=True): | ||||
text = self.PROMPT + text | text = self.PROMPT + text | ||||
if default: | if default: | ||||
text += " \x1b[33m[{0}]\x1b[0m".format(default) | |||||
text += f" \x1b[33m[{default}]\x1b[0m" | |||||
lines = wrap(re.sub(r"\s\s+", " ", text), self.WIDTH) | lines = wrap(re.sub(r"\s\s+", " ", text), self.WIDTH) | ||||
if len(lines) > 1: | if len(lines) > 1: | ||||
print("\n".join(lines[:-1])) | print("\n".join(lines[:-1])) | ||||
@@ -157,7 +159,9 @@ class ConfigScript: | |||||
salt=salt, | salt=salt, | ||||
iterations=self.PBKDF_ROUNDS, | iterations=self.PBKDF_ROUNDS, | ||||
) | ) | ||||
self._cipher = fernet.Fernet(base64.urlsafe_b64encode(kdf.derive(key.encode()))) | |||||
self._cipher = fernet.Fernet( | |||||
base64.urlsafe_b64encode(kdf.derive(key.encode())) | |||||
) | |||||
except ImportError: | except ImportError: | ||||
print(" error!") | print(" error!") | ||||
self._print("""Encryption requires the 'cryptography' package: | self._print("""Encryption requires the 'cryptography' package: | ||||
@@ -198,7 +202,7 @@ class ConfigScript: | |||||
front-end.""") | front-end.""") | ||||
frontend = self._ask_bool("Enable the IRC front-end?") | frontend = self._ask_bool("Enable the IRC front-end?") | ||||
watcher = self._ask_bool("Enable the IRC watcher?") | watcher = self._ask_bool("Enable the IRC watcher?") | ||||
scheduler = self._ask_bool("Enable the wiki task scheduler?") | |||||
scheduler = self._ask_bool("Enable the wiki task scheduler?") | |||||
self.data["components"]["irc_frontend"] = frontend | self.data["components"]["irc_frontend"] = frontend | ||||
self.data["components"]["irc_watcher"] = watcher | self.data["components"]["irc_watcher"] = watcher | ||||
self.data["components"]["wiki_scheduler"] = scheduler | self.data["components"]["wiki_scheduler"] = scheduler | ||||
@@ -269,7 +273,9 @@ class ConfigScript: | |||||
self.data["wiki"]["username"] = self._ask("Bot username:") | self.data["wiki"]["username"] = self._ask("Bot username:") | ||||
password = self._ask_pass("Bot password:", encrypt=False) | password = self._ask_pass("Bot password:", encrypt=False) | ||||
self.data["wiki"]["password"] = password | self.data["wiki"]["password"] = password | ||||
self.data["wiki"]["userAgent"] = "EarwigBot/$1 (Python/$2; https://github.com/earwig/earwigbot)" | |||||
self.data["wiki"]["userAgent"] = ( | |||||
"EarwigBot/$1 (Python/$2; https://github.com/earwig/earwigbot)" | |||||
) | |||||
self.data["wiki"]["summary"] = "([[WP:BOT|Bot]]) $2" | self.data["wiki"]["summary"] = "([[WP:BOT|Bot]]) $2" | ||||
self.data["wiki"]["useHTTPS"] = True | self.data["wiki"]["useHTTPS"] = True | ||||
self.data["wiki"]["assert"] = "user" | self.data["wiki"]["assert"] = "user" | ||||
@@ -282,15 +288,17 @@ class ConfigScript: | |||||
msg = "Will this bot run from the Wikimedia Tool Labs?" | msg = "Will this bot run from the Wikimedia Tool Labs?" | ||||
labs = self._ask_bool(msg, default=False) | labs = self._ask_bool(msg, default=False) | ||||
if labs: | if labs: | ||||
args = [("host", "$1.labsdb"), ("db", "$1_p"), | |||||
("read_default_file", "~/replica.my.cnf")] | |||||
args = [ | |||||
("host", "$1.labsdb"), | |||||
("db", "$1_p"), | |||||
("read_default_file", "~/replica.my.cnf"), | |||||
] | |||||
self.data["wiki"]["sql"] = OrderedDict(args) | self.data["wiki"]["sql"] = OrderedDict(args) | ||||
else: | else: | ||||
msg = "Will this bot run from the Wikimedia Toolserver?" | msg = "Will this bot run from the Wikimedia Toolserver?" | ||||
toolserver = self._ask_bool(msg, default=False) | toolserver = self._ask_bool(msg, default=False) | ||||
if toolserver: | if toolserver: | ||||
args = [("host", "$1-p.rrdb.toolserver.org"), | |||||
("db", "$1_p")] | |||||
args = [("host", "$1-p.rrdb.toolserver.org"), ("db", "$1_p")] | |||||
self.data["wiki"]["sql"] = OrderedDict(args) | self.data["wiki"]["sql"] = OrderedDict(args) | ||||
self.data["wiki"]["shutoff"] = {} | self.data["wiki"]["shutoff"] = {} | ||||
@@ -314,11 +322,13 @@ class ConfigScript: | |||||
print() | print() | ||||
frontend = self.data["irc"]["frontend"] = OrderedDict() | frontend = self.data["irc"]["frontend"] = OrderedDict() | ||||
frontend["host"] = self._ask( | frontend["host"] = self._ask( | ||||
"Hostname of the frontend's IRC server:", "irc.libera.chat") | |||||
"Hostname of the frontend's IRC server:", "irc.libera.chat" | |||||
) | |||||
frontend["port"] = self._ask("Frontend port:", 6667) | frontend["port"] = self._ask("Frontend port:", 6667) | ||||
frontend["nick"] = self._ask("Frontend bot's nickname:") | frontend["nick"] = self._ask("Frontend bot's nickname:") | ||||
frontend["ident"] = self._ask( | frontend["ident"] = self._ask( | ||||
"Frontend bot's ident:", frontend["nick"].lower()) | |||||
"Frontend bot's ident:", frontend["nick"].lower() | |||||
) | |||||
question = "Frontend bot's real name (gecos):" | question = "Frontend bot's real name (gecos):" | ||||
frontend["realname"] = self._ask(question, "EarwigBot") | frontend["realname"] = self._ask(question, "EarwigBot") | ||||
if self._ask_bool("Should the bot identify to NickServ?"): | if self._ask_bool("Should the bot identify to NickServ?"): | ||||
@@ -370,7 +380,7 @@ class ConfigScript: | |||||
watcher["nickservUsername"] = ns_user | watcher["nickservUsername"] = ns_user | ||||
watcher["nickservPassword"] = ns_pass | watcher["nickservPassword"] = ns_pass | ||||
if self._wmf: | if self._wmf: | ||||
chan = "#{0}.{1}".format(self._lang, self._proj) | |||||
chan = f"#{self._lang}.{self._proj}" | |||||
watcher["channels"] = [chan] | watcher["channels"] = [chan] | ||||
else: | else: | ||||
chan_question = "Watcher channels to join by default:" | chan_question = "Watcher channels to join by default:" | ||||
@@ -387,14 +397,17 @@ class ConfigScript: | |||||
fp.write(RULES_TEMPLATE) | fp.write(RULES_TEMPLATE) | ||||
self._pause() | self._pause() | ||||
self.data["irc"]["version"] = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | |||||
self.data["irc"]["version"] = ( | |||||
"EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | |||||
) | |||||
def _set_commands(self): | def _set_commands(self): | ||||
print() | print() | ||||
msg = """Would you like to disable the default IRC commands? You can | msg = """Would you like to disable the default IRC commands? You can | ||||
fine-tune which commands are disabled later on.""" | fine-tune which commands are disabled later on.""" | ||||
if (not self.data["components"]["irc_frontend"] or | |||||
self._ask_bool(msg, default=False)): | |||||
if not self.data["components"]["irc_frontend"] or self._ask_bool( | |||||
msg, default=False | |||||
): | |||||
self.data["commands"]["disable"] = True | self.data["commands"]["disable"] = True | ||||
print() | print() | ||||
self._print("""I am now creating the 'commands/' directory, where you | self._print("""I am now creating the 'commands/' directory, where you | ||||
@@ -435,8 +448,14 @@ class ConfigScript: | |||||
def _save(self): | def _save(self): | ||||
with open(self.config.path, "w") as stream: | with open(self.config.path, "w") as stream: | ||||
yaml.dump(self.data, stream, OrderedDumper, indent=4, | |||||
allow_unicode=True, default_flow_style=False) | |||||
yaml.dump( | |||||
self.data, | |||||
stream, | |||||
OrderedDumper, | |||||
indent=4, | |||||
allow_unicode=True, | |||||
default_flow_style=False, | |||||
) | |||||
def make_new(self): | def make_new(self): | ||||
"""Make a new config file based on the user's input.""" | """Make a new config file based on the user's input.""" | ||||
@@ -447,8 +466,8 @@ class ConfigScript: | |||||
raise | raise | ||||
try: | try: | ||||
open(self.config.path, "w").close() | open(self.config.path, "w").close() | ||||
chmod(self.config.path, stat.S_IRUSR|stat.S_IWUSR) | |||||
except IOError: | |||||
chmod(self.config.path, stat.S_IRUSR | stat.S_IWUSR) | |||||
except OSError: | |||||
print("I can't seem to write to the config file:") | print("I can't seem to write to the config file:") | ||||
raise | raise | ||||
self._set_metadata() | self._set_metadata() | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -55,9 +53,11 @@ This module contains all exceptions used by EarwigBot:: | |||||
+-- ParserExclusionError | +-- ParserExclusionError | ||||
""" | """ | ||||
class EarwigBotError(Exception): | class EarwigBotError(Exception): | ||||
"""Base exception class for errors in EarwigBot.""" | """Base exception class for errors in EarwigBot.""" | ||||
class NoConfigError(EarwigBotError): | class NoConfigError(EarwigBotError): | ||||
"""The bot cannot be run without a config file. | """The bot cannot be run without a config file. | ||||
@@ -65,9 +65,11 @@ class NoConfigError(EarwigBotError): | |||||
one to be created. | one to be created. | ||||
""" | """ | ||||
class IRCError(EarwigBotError): | class IRCError(EarwigBotError): | ||||
"""Base exception class for errors in IRC-relation sections of the bot.""" | """Base exception class for errors in IRC-relation sections of the bot.""" | ||||
class BrokenSocketError(IRCError): | class BrokenSocketError(IRCError): | ||||
"""A socket has broken, because it is not sending data. | """A socket has broken, because it is not sending data. | ||||
@@ -75,15 +77,18 @@ class BrokenSocketError(IRCError): | |||||
<earwigbot.irc.connection.IRCConnection._get>`. | <earwigbot.irc.connection.IRCConnection._get>`. | ||||
""" | """ | ||||
class WikiToolsetError(EarwigBotError): | class WikiToolsetError(EarwigBotError): | ||||
"""Base exception class for errors in the Wiki Toolset.""" | """Base exception class for errors in the Wiki Toolset.""" | ||||
class SiteNotFoundError(WikiToolsetError): | class SiteNotFoundError(WikiToolsetError): | ||||
"""A particular site could not be found in the sites database. | """A particular site could not be found in the sites database. | ||||
Raised by :py:class:`~earwigbot.wiki.sitesdb.SitesDB`. | Raised by :py:class:`~earwigbot.wiki.sitesdb.SitesDB`. | ||||
""" | """ | ||||
class ServiceError(WikiToolsetError): | class ServiceError(WikiToolsetError): | ||||
"""Base exception class for an error within a service (the API or SQL). | """Base exception class for an error within a service (the API or SQL). | ||||
@@ -92,6 +97,7 @@ class ServiceError(WikiToolsetError): | |||||
non-functional so another, less-preferred one can be tried. | non-functional so another, less-preferred one can be tried. | ||||
""" | """ | ||||
class APIError(ServiceError): | class APIError(ServiceError): | ||||
"""Couldn't connect to a site's API. | """Couldn't connect to a site's API. | ||||
@@ -101,18 +107,21 @@ class APIError(ServiceError): | |||||
Raised by :py:meth:`Site.api_query <earwigbot.wiki.site.Site.api_query>`. | Raised by :py:meth:`Site.api_query <earwigbot.wiki.site.Site.api_query>`. | ||||
""" | """ | ||||
class SQLError(ServiceError): | class SQLError(ServiceError): | ||||
"""Some error involving SQL querying occurred. | """Some error involving SQL querying occurred. | ||||
Raised by :py:meth:`Site.sql_query <earwigbot.wiki.site.Site.sql_query>`. | Raised by :py:meth:`Site.sql_query <earwigbot.wiki.site.Site.sql_query>`. | ||||
""" | """ | ||||
class NoServiceError(WikiToolsetError): | class NoServiceError(WikiToolsetError): | ||||
"""No service is functioning to handle a specific task. | """No service is functioning to handle a specific task. | ||||
Raised by :py:meth:`Site.delegate <earwigbot.wiki.site.Site.delegate>`. | Raised by :py:meth:`Site.delegate <earwigbot.wiki.site.Site.delegate>`. | ||||
""" | """ | ||||
class LoginError(WikiToolsetError): | class LoginError(WikiToolsetError): | ||||
"""An error occured while trying to login. | """An error occured while trying to login. | ||||
@@ -121,6 +130,7 @@ class LoginError(WikiToolsetError): | |||||
Raised by :py:meth:`Site._login <earwigbot.wiki.site.Site._login>`. | Raised by :py:meth:`Site._login <earwigbot.wiki.site.Site._login>`. | ||||
""" | """ | ||||
class PermissionsError(WikiToolsetError): | class PermissionsError(WikiToolsetError): | ||||
"""A permissions error ocurred. | """A permissions error ocurred. | ||||
@@ -134,6 +144,7 @@ class PermissionsError(WikiToolsetError): | |||||
other API methods depending on settings. | other API methods depending on settings. | ||||
""" | """ | ||||
class NamespaceNotFoundError(WikiToolsetError): | class NamespaceNotFoundError(WikiToolsetError): | ||||
"""A requested namespace name or namespace ID does not exist. | """A requested namespace name or namespace ID does not exist. | ||||
@@ -143,18 +154,21 @@ class NamespaceNotFoundError(WikiToolsetError): | |||||
<earwigbot.wiki.site.Site.namespace_name_to_id>`. | <earwigbot.wiki.site.Site.namespace_name_to_id>`. | ||||
""" | """ | ||||
class PageNotFoundError(WikiToolsetError): | class PageNotFoundError(WikiToolsetError): | ||||
"""Attempted to get information about a page that does not exist. | """Attempted to get information about a page that does not exist. | ||||
Raised by :py:class:`~earwigbot.wiki.page.Page`. | Raised by :py:class:`~earwigbot.wiki.page.Page`. | ||||
""" | """ | ||||
class InvalidPageError(WikiToolsetError): | class InvalidPageError(WikiToolsetError): | ||||
"""Attempted to get information about a page whose title is invalid. | """Attempted to get information about a page whose title is invalid. | ||||
Raised by :py:class:`~earwigbot.wiki.page.Page`. | Raised by :py:class:`~earwigbot.wiki.page.Page`. | ||||
""" | """ | ||||
class RedirectError(WikiToolsetError): | class RedirectError(WikiToolsetError): | ||||
"""A redirect-only method was called on a malformed or non-redirect page. | """A redirect-only method was called on a malformed or non-redirect page. | ||||
@@ -162,12 +176,14 @@ class RedirectError(WikiToolsetError): | |||||
<earwigbot.wiki.page.Page.get_redirect_target>`. | <earwigbot.wiki.page.Page.get_redirect_target>`. | ||||
""" | """ | ||||
class UserNotFoundError(WikiToolsetError): | class UserNotFoundError(WikiToolsetError): | ||||
"""Attempted to get certain information about a user that does not exist. | """Attempted to get certain information about a user that does not exist. | ||||
Raised by :py:class:`~earwigbot.wiki.user.User`. | Raised by :py:class:`~earwigbot.wiki.user.User`. | ||||
""" | """ | ||||
class EditError(WikiToolsetError): | class EditError(WikiToolsetError): | ||||
"""An error occured while editing. | """An error occured while editing. | ||||
@@ -178,6 +194,7 @@ class EditError(WikiToolsetError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class EditConflictError(EditError): | class EditConflictError(EditError): | ||||
"""We gotten an edit conflict or a (rarer) delete/recreate conflict. | """We gotten an edit conflict or a (rarer) delete/recreate conflict. | ||||
@@ -185,6 +202,7 @@ class EditConflictError(EditError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class NoContentError(EditError): | class NoContentError(EditError): | ||||
"""We tried to create a page or new section with no content. | """We tried to create a page or new section with no content. | ||||
@@ -192,6 +210,7 @@ class NoContentError(EditError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class ContentTooBigError(EditError): | class ContentTooBigError(EditError): | ||||
"""The edit we tried to push exceeded the article size limit. | """The edit we tried to push exceeded the article size limit. | ||||
@@ -199,6 +218,7 @@ class ContentTooBigError(EditError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class SpamDetectedError(EditError): | class SpamDetectedError(EditError): | ||||
"""The spam filter refused our edit. | """The spam filter refused our edit. | ||||
@@ -206,6 +226,7 @@ class SpamDetectedError(EditError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class FilteredError(EditError): | class FilteredError(EditError): | ||||
"""The edit filter refused our edit. | """The edit filter refused our edit. | ||||
@@ -213,6 +234,7 @@ class FilteredError(EditError): | |||||
:py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | :py:meth:`Page.add_section <earwigbot.wiki.page.Page.add_section>`. | ||||
""" | """ | ||||
class CopyvioCheckError(WikiToolsetError): | class CopyvioCheckError(WikiToolsetError): | ||||
"""An error occured when checking a page for copyright violations. | """An error occured when checking a page for copyright violations. | ||||
@@ -225,6 +247,7 @@ class CopyvioCheckError(WikiToolsetError): | |||||
<earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_compare>`. | <earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_compare>`. | ||||
""" | """ | ||||
class UnknownSearchEngineError(CopyvioCheckError): | class UnknownSearchEngineError(CopyvioCheckError): | ||||
"""Attempted to do a copyvio check with an unknown search engine. | """Attempted to do a copyvio check with an unknown search engine. | ||||
@@ -235,6 +258,7 @@ class UnknownSearchEngineError(CopyvioCheckError): | |||||
<earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | <earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | ||||
""" | """ | ||||
class UnsupportedSearchEngineError(CopyvioCheckError): | class UnsupportedSearchEngineError(CopyvioCheckError): | ||||
"""Attmpted to do a copyvio check using an unavailable engine. | """Attmpted to do a copyvio check using an unavailable engine. | ||||
@@ -245,6 +269,7 @@ class UnsupportedSearchEngineError(CopyvioCheckError): | |||||
<earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | <earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | ||||
""" | """ | ||||
class SearchQueryError(CopyvioCheckError): | class SearchQueryError(CopyvioCheckError): | ||||
"""Some error ocurred while doing a search query. | """Some error ocurred while doing a search query. | ||||
@@ -252,6 +277,7 @@ class SearchQueryError(CopyvioCheckError): | |||||
<earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | <earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`. | ||||
""" | """ | ||||
class ParserExclusionError(CopyvioCheckError): | class ParserExclusionError(CopyvioCheckError): | ||||
"""A content parser detected that the given source should be excluded. | """A content parser detected that the given source should be excluded. | ||||
@@ -260,6 +286,7 @@ class ParserExclusionError(CopyvioCheckError): | |||||
exposed in client code. | exposed in client code. | ||||
""" | """ | ||||
class ParserRedirectError(CopyvioCheckError): | class ParserRedirectError(CopyvioCheckError): | ||||
"""A content parser detected that a redirect should be followed. | """A content parser detected that a redirect should be followed. | ||||
@@ -267,6 +294,7 @@ class ParserRedirectError(CopyvioCheckError): | |||||
<earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`; should not be | <earwigbot.wiki.copyvios.CopyvioMixIn.copyvio_check>`; should not be | ||||
exposed in client code. | exposed in client code. | ||||
""" | """ | ||||
def __init__(self, url): | def __init__(self, url): | ||||
super().__init__() | super().__init__() | ||||
self.url = url | self.url = url |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -28,6 +26,7 @@ from earwigbot.exceptions import BrokenSocketError | |||||
__all__ = ["IRCConnection"] | __all__ = ["IRCConnection"] | ||||
class IRCConnection: | class IRCConnection: | ||||
"""Interface with an IRC server.""" | """Interface with an IRC server.""" | ||||
@@ -50,8 +49,7 @@ class IRCConnection: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the IRCConnection.""" | """Return the canonical string representation of the IRCConnection.""" | ||||
res = "IRCConnection(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r})" | res = "IRCConnection(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r})" | ||||
return res.format(self.host, self.port, self.nick, self.ident, | |||||
self.realname) | |||||
return res.format(self.host, self.port, self.nick, self.ident, self.realname) | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the IRCConnection.""" | """Return a nice string representation of the IRCConnection.""" | ||||
@@ -63,18 +61,18 @@ class IRCConnection: | |||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
try: | try: | ||||
self._sock.connect((self.host, self.port)) | self._sock.connect((self.host, self.port)) | ||||
except socket.error: | |||||
except OSError: | |||||
self.logger.exception("Couldn't connect to IRC server; retrying") | self.logger.exception("Couldn't connect to IRC server; retrying") | ||||
sleep(8) | sleep(8) | ||||
self._connect() | self._connect() | ||||
self._send("NICK {0}".format(self.nick)) | |||||
self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname)) | |||||
self._send(f"NICK {self.nick}") | |||||
self._send(f"USER {self.ident} {self.host} * :{self.realname}") | |||||
def _close(self): | def _close(self): | ||||
"""Completely close our connection with the IRC server.""" | """Completely close our connection with the IRC server.""" | ||||
try: | try: | ||||
self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first | self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first | ||||
except socket.error: | |||||
except OSError: | |||||
pass # Ignore if the socket is already down | pass # Ignore if the socket is already down | ||||
self._sock.close() | self._sock.close() | ||||
@@ -94,7 +92,7 @@ class IRCConnection: | |||||
sleep(0.75 - time_since_last) | sleep(0.75 - time_since_last) | ||||
try: | try: | ||||
self._sock.sendall(msg.encode() + b"\r\n") | self._sock.sendall(msg.encode() + b"\r\n") | ||||
except socket.error: | |||||
except OSError: | |||||
self._is_running = False | self._is_running = False | ||||
else: | else: | ||||
if not hidelog: | if not hidelog: | ||||
@@ -131,7 +129,7 @@ class IRCConnection: | |||||
def _quit(self, msg=None): | def _quit(self, msg=None): | ||||
"""Issue a quit message to the server. Doesn't close the connection.""" | """Issue a quit message to the server. Doesn't close the connection.""" | ||||
if msg: | if msg: | ||||
self._send("QUIT :{0}".format(msg)) | |||||
self._send(f"QUIT :{msg}") | |||||
else: | else: | ||||
self._send("QUIT") | self._send("QUIT") | ||||
@@ -142,11 +140,10 @@ class IRCConnection: | |||||
self.pong(line[1][1:]) | self.pong(line[1][1:]) | ||||
elif line[1] == "001": # Update nickname on startup | elif line[1] == "001": # Update nickname on startup | ||||
if line[2] != self.nick: | if line[2] != self.nick: | ||||
self.logger.warn("Nickname changed from {0} to {1}".format( | |||||
self.nick, line[2])) | |||||
self.logger.warn(f"Nickname changed from {self.nick} to {line[2]}") | |||||
self._nick = line[2] | self._nick = line[2] | ||||
elif line[1] == "376": # After sign-on, get our userhost | elif line[1] == "376": # After sign-on, get our userhost | ||||
self._send("WHOIS {0}".format(self.nick)) | |||||
self._send(f"WHOIS {self.nick}") | |||||
elif line[1] == "311": # Receiving WHOIS result | elif line[1] == "311": # Receiving WHOIS result | ||||
if line[2] == self.nick: | if line[2] == self.nick: | ||||
self._ident = line[4] | self._ident = line[4] | ||||
@@ -189,7 +186,7 @@ class IRCConnection: | |||||
def say(self, target, msg, hidelog=False): | def say(self, target, msg, hidelog=False): | ||||
"""Send a private message to a target on the server.""" | """Send a private message to a target on the server.""" | ||||
for msg in self._split(msg, len(target) + 10): | for msg in self._split(msg, len(target) + 10): | ||||
msg = "PRIVMSG {0} :{1}".format(target, msg) | |||||
msg = f"PRIVMSG {target} :{msg}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def reply(self, data, msg, hidelog=False): | def reply(self, data, msg, hidelog=False): | ||||
@@ -197,45 +194,45 @@ class IRCConnection: | |||||
if data.is_private: | if data.is_private: | ||||
self.say(data.chan, msg, hidelog) | self.say(data.chan, msg, hidelog) | ||||
else: | else: | ||||
msg = "\x02{0}\x0F: {1}".format(data.reply_nick, msg) | |||||
msg = f"\x02{data.reply_nick}\x0f: {msg}" | |||||
self.say(data.chan, msg, hidelog) | self.say(data.chan, msg, hidelog) | ||||
def action(self, target, msg, hidelog=False): | def action(self, target, msg, hidelog=False): | ||||
"""Send a private message to a target on the server as an action.""" | """Send a private message to a target on the server as an action.""" | ||||
msg = "\x01ACTION {0}\x01".format(msg) | |||||
msg = f"\x01ACTION {msg}\x01" | |||||
self.say(target, msg, hidelog) | self.say(target, msg, hidelog) | ||||
def notice(self, target, msg, hidelog=False): | def notice(self, target, msg, hidelog=False): | ||||
"""Send a notice to a target on the server.""" | """Send a notice to a target on the server.""" | ||||
for msg in self._split(msg, len(target) + 9): | for msg in self._split(msg, len(target) + 9): | ||||
msg = "NOTICE {0} :{1}".format(target, msg) | |||||
msg = f"NOTICE {target} :{msg}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def join(self, chan, hidelog=False): | def join(self, chan, hidelog=False): | ||||
"""Join a channel on the server.""" | """Join a channel on the server.""" | ||||
msg = "JOIN {0}".format(chan) | |||||
msg = f"JOIN {chan}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def part(self, chan, msg=None, hidelog=False): | def part(self, chan, msg=None, hidelog=False): | ||||
"""Part from a channel on the server, optionally using an message.""" | """Part from a channel on the server, optionally using an message.""" | ||||
if msg: | if msg: | ||||
self._send("PART {0} :{1}".format(chan, msg), hidelog) | |||||
self._send(f"PART {chan} :{msg}", hidelog) | |||||
else: | else: | ||||
self._send("PART {0}".format(chan), hidelog) | |||||
self._send(f"PART {chan}", hidelog) | |||||
def mode(self, target, level, msg, hidelog=False): | def mode(self, target, level, msg, hidelog=False): | ||||
"""Send a mode message to the server.""" | """Send a mode message to the server.""" | ||||
msg = "MODE {0} {1} {2}".format(target, level, msg) | |||||
msg = f"MODE {target} {level} {msg}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def ping(self, target, hidelog=False): | def ping(self, target, hidelog=False): | ||||
"""Ping another entity on the server.""" | """Ping another entity on the server.""" | ||||
msg = "PING {0}".format(target) | |||||
msg = f"PING {target}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def pong(self, target, hidelog=False): | def pong(self, target, hidelog=False): | ||||
"""Pong another entity on the server.""" | """Pong another entity on the server.""" | ||||
msg = "PONG {0}".format(target) | |||||
msg = f"PONG {target}" | |||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
def loop(self): | def loop(self): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,6 +22,7 @@ import re | |||||
__all__ = ["Data"] | __all__ = ["Data"] | ||||
class Data: | class Data: | ||||
"""Store data from an individual line received on IRC.""" | """Store data from an individual line received on IRC.""" | ||||
@@ -46,7 +45,7 @@ class Data: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Data.""" | """Return a nice string representation of the Data.""" | ||||
return "<Data of {0!r}>".format(" ".join(self.line)) | |||||
return "<Data of {!r}>".format(" ".join(self.line)) | |||||
def _parse(self): | def _parse(self): | ||||
"""Parse a line from IRC into its components as instance attributes.""" | """Parse a line from IRC into its components as instance attributes.""" | ||||
@@ -97,8 +96,7 @@ class Data: | |||||
self._is_command = True | self._is_command = True | ||||
self._trigger = self.command[0] | self._trigger = self.command[0] | ||||
self._command = self.command[1:] # Strip the "!" or "." | self._command = self.command[1:] # Strip the "!" or "." | ||||
elif re.match(r"{0}\W*?$".format(re.escape(self.my_nick)), | |||||
self.command, re.U): | |||||
elif re.match(rf"{re.escape(self.my_nick)}\W*?$", self.command, re.U): | |||||
# e.g. "EarwigBot, command arg1 arg2" | # e.g. "EarwigBot, command arg1 arg2" | ||||
self._is_command = True | self._is_command = True | ||||
self._trigger = self.my_nick | self._trigger = self.my_nick | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,10 +20,11 @@ | |||||
from time import sleep | from time import sleep | ||||
from earwigbot.irc import IRCConnection, Data | |||||
from earwigbot.irc import Data, IRCConnection | |||||
__all__ = ["Frontend"] | __all__ = ["Frontend"] | ||||
class Frontend(IRCConnection): | class Frontend(IRCConnection): | ||||
""" | """ | ||||
**EarwigBot: IRC Frontend Component** | **EarwigBot: IRC Frontend Component** | ||||
@@ -38,13 +37,20 @@ class Frontend(IRCConnection): | |||||
:py:mod:`earwigbot.commands` or the bot's custom command directory | :py:mod:`earwigbot.commands` or the bot's custom command directory | ||||
(explained in the :doc:`documentation </customizing>`). | (explained in the :doc:`documentation </customizing>`). | ||||
""" | """ | ||||
NICK_SERVICES = "NickServ" | NICK_SERVICES = "NickServ" | ||||
def __init__(self, bot): | def __init__(self, bot): | ||||
self.bot = bot | self.bot = bot | ||||
cf = bot.config.irc["frontend"] | cf = bot.config.irc["frontend"] | ||||
super().__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||||
cf["realname"], bot.logger.getChild("frontend")) | |||||
super().__init__( | |||||
cf["host"], | |||||
cf["port"], | |||||
cf["nick"], | |||||
cf["ident"], | |||||
cf["realname"], | |||||
bot.logger.getChild("frontend"), | |||||
) | |||||
self._auth_wait = False | self._auth_wait = False | ||||
self._channels = set() | self._channels = set() | ||||
@@ -53,8 +59,9 @@ class Frontend(IRCConnection): | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the Frontend.""" | """Return the canonical string representation of the Frontend.""" | ||||
res = "Frontend(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r}, bot={5!r})" | res = "Frontend(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r}, bot={5!r})" | ||||
return res.format(self.host, self.port, self.nick, self.ident, | |||||
self.realname, self.bot) | |||||
return res.format( | |||||
self.host, self.port, self.nick, self.ident, self.realname, self.bot | |||||
) | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Frontend.""" | """Return a nice string representation of the Frontend.""" | ||||
@@ -133,7 +140,7 @@ class Frontend(IRCConnection): | |||||
self._join_channels() | self._join_channels() | ||||
else: | else: | ||||
self.logger.debug("Identifying with services") | self.logger.debug("Identifying with services") | ||||
msg = "IDENTIFY {0} {1}".format(username, password) | |||||
msg = f"IDENTIFY {username} {password}" | |||||
self.say(self.NICK_SERVICES, msg, hidelog=True) | self.say(self.NICK_SERVICES, msg, hidelog=True) | ||||
self._auth_wait = True | self._auth_wait = True | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2021 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,14 +22,18 @@ import re | |||||
__all__ = ["RC"] | __all__ = ["RC"] | ||||
class RC: | class RC: | ||||
"""Store data from an event received from our IRC watcher.""" | """Store data from an event received from our IRC watcher.""" | ||||
re_color = re.compile("\x03([0-9]{1,2}(,[0-9]{1,2})?)?") | re_color = re.compile("\x03([0-9]{1,2}(,[0-9]{1,2})?)?") | ||||
re_edit = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s(https?://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z") | |||||
re_edit = re.compile( | |||||
"\A\[\[(.*?)\]\]\s(.*?)\s(https?://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z" | |||||
) | |||||
re_log = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s\s\*\s(.*?)\s\*\s(.*?)\Z") | re_log = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s\s\*\s(.*?)\s\*\s(.*?)\Z") | ||||
pretty_edit = "\x02New {0}\x0F: \x0314[[\x0307{1}\x0314]]\x0306 * \x0303{2}\x0306 * \x0302{3}\x0306 * \x0310{4}" | |||||
pretty_log = "\x02New {0}\x0F: \x0303{1}\x0306 * \x0302{2}\x0306 * \x0310{3}" | |||||
pretty_edit = "\x02New {0}\x0f: \x0314[[\x0307{1}\x0314]]\x0306 * \x0303{2}\x0306 * \x0302{3}\x0306 * \x0310{4}" | |||||
pretty_log = "\x02New {0}\x0f: \x0303{1}\x0306 * \x0302{2}\x0306 * \x0310{3}" | |||||
plain_edit = "New {0}: [[{1}]] * {2} * {3} * {4}" | plain_edit = "New {0}: [[{1}]] * {2} * {3} * {4}" | ||||
plain_log = "New {0}: {1} * {2} * {3}" | plain_log = "New {0}: {1} * {2} * {3}" | ||||
@@ -41,11 +43,11 @@ class RC: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the RC.""" | """Return the canonical string representation of the RC.""" | ||||
return "RC(chan={0!r}, msg={1!r})".format(self.chan, self.msg) | |||||
return f"RC(chan={self.chan!r}, msg={self.msg!r})" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the RC.""" | """Return a nice string representation of the RC.""" | ||||
return "<RC of {0!r} on {1}>".format(self.msg, self.chan) | |||||
return f"<RC of {self.msg!r} on {self.chan}>" | |||||
def parse(self): | def parse(self): | ||||
"""Parse a recent change event into some variables.""" | """Parse a recent change event into some variables.""" | ||||
@@ -62,7 +64,7 @@ class RC: | |||||
# We're probably missing the https:// part, because it's a log | # We're probably missing the https:// part, because it's a log | ||||
# entry, which lacks a URL: | # entry, which lacks a URL: | ||||
page, flags, user, comment = self.re_log.findall(msg)[0] | page, flags, user, comment = self.re_log.findall(msg)[0] | ||||
url = "https://{0}.org/wiki/{1}".format(self.chan[1:], page) | |||||
url = f"https://{self.chan[1:]}.org/wiki/{page}" | |||||
self.is_edit = False # This is a log entry, not edit | self.is_edit = False # This is a log entry, not edit | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -23,7 +21,7 @@ | |||||
import importlib.machinery | import importlib.machinery | ||||
import importlib.util | import importlib.util | ||||
from earwigbot.irc import IRCConnection, RC | |||||
from earwigbot.irc import RC, IRCConnection | |||||
__all__ = ["Watcher"] | __all__ = ["Watcher"] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -1,6 +1,4 @@ | |||||
#! /usr/bin/env python | #! /usr/bin/env python | ||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -69,12 +67,11 @@ class _ResourceManager: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the manager.""" | """Return a nice string representation of the manager.""" | ||||
return "<{0} of {1}>".format(self.__class__.__name__, self.bot) | |||||
return f"<{self.__class__.__name__} of {self.bot}>" | |||||
def __iter__(self): | def __iter__(self): | ||||
with self.lock: | with self.lock: | ||||
for resource in self._resources.values(): | |||||
yield resource | |||||
yield from self._resources.values() | |||||
def _is_disabled(self, name): | def _is_disabled(self, name): | ||||
"""Check whether a resource should be disabled.""" | """Check whether a resource should be disabled.""" | ||||
@@ -99,7 +96,7 @@ class _ResourceManager: | |||||
self.logger.exception(e.format(res_type, name, path)) | self.logger.exception(e.format(res_type, name, path)) | ||||
else: | else: | ||||
self._resources[resource.name] = resource | self._resources[resource.name] = resource | ||||
self.logger.debug("Loaded {0} {1}".format(res_type, resource.name)) | |||||
self.logger.debug(f"Loaded {res_type} {resource.name}") | |||||
def _load_module(self, name, path): | def _load_module(self, name, path): | ||||
"""Load a specific resource from a module, identified by name and path. | """Load a specific resource from a module, identified by name and path. | ||||
@@ -124,12 +121,12 @@ class _ResourceManager: | |||||
for obj in vars(module).values(): | for obj in vars(module).values(): | ||||
if type(obj) is type: | if type(obj) is type: | ||||
isresource = issubclass(obj, self._resource_base) | isresource = issubclass(obj, self._resource_base) | ||||
if isresource and not obj is self._resource_base: | |||||
if isresource and obj is not self._resource_base: | |||||
self._load_resource(name, path, obj) | self._load_resource(name, path, obj) | ||||
def _load_directory(self, dir): | def _load_directory(self, dir): | ||||
"""Load all valid resources in a given directory.""" | """Load all valid resources in a given directory.""" | ||||
self.logger.debug("Loading directory {0}".format(dir)) | |||||
self.logger.debug(f"Loading directory {dir}") | |||||
processed = [] | processed = [] | ||||
for name in listdir(dir): | for name in listdir(dir): | ||||
if not name.endswith(".py") and not name.endswith(".pyc"): | if not name.endswith(".py") and not name.endswith(".pyc"): | ||||
@@ -141,7 +138,7 @@ class _ResourceManager: | |||||
continue | continue | ||||
processed.append(modname) | processed.append(modname) | ||||
if self._is_disabled(modname): | if self._is_disabled(modname): | ||||
log = "Skipping disabled module {0}".format(modname) | |||||
log = f"Skipping disabled module {modname}" | |||||
self.logger.debug(log) | self.logger.debug(log) | ||||
continue | continue | ||||
self._load_module(modname, dir) | self._load_module(modname, dir) | ||||
@@ -188,7 +185,7 @@ class _ResourceManager: | |||||
resources = ", ".join(self._resources.keys()) | resources = ", ".join(self._resources.keys()) | ||||
self.logger.info(msg.format(len(self._resources), name, resources)) | self.logger.info(msg.format(len(self._resources), name, resources)) | ||||
else: | else: | ||||
self.logger.info("Loaded 0 {0}".format(name)) | |||||
self.logger.info(f"Loaded 0 {name}") | |||||
def get(self, key): | def get(self, key): | ||||
"""Return the class instance associated with a certain resource. | """Return the class instance associated with a certain resource. | ||||
@@ -241,7 +238,7 @@ class CommandManager(_ResourceManager): | |||||
if hook in command.hooks and self._wrap_check(command, data): | if hook in command.hooks and self._wrap_check(command, data): | ||||
thread = Thread(target=self._wrap_process, args=(command, data)) | thread = Thread(target=self._wrap_process, args=(command, data)) | ||||
start_time = strftime("%b %d %H:%M:%S") | start_time = strftime("%b %d %H:%M:%S") | ||||
thread.name = "irc:{0} ({1})".format(command.name, start_time) | |||||
thread.name = f"irc:{command.name} ({start_time})" | |||||
thread.daemon = True | thread.daemon = True | ||||
thread.start() | thread.start() | ||||
return | return | ||||
@@ -287,7 +284,7 @@ class TaskManager(_ResourceManager): | |||||
task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) | task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) | ||||
start_time = strftime("%b %d %H:%M:%S") | start_time = strftime("%b %d %H:%M:%S") | ||||
task_thread.name = "{0} ({1})".format(task_name, start_time) | |||||
task_thread.name = f"{task_name} ({start_time})" | |||||
task_thread.daemon = True | task_thread.daemon = True | ||||
task_thread.start() | task_thread.start() | ||||
return task_thread | return task_thread | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,10 +19,10 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot import wiki | |||||
__all__ = ["Task"] | __all__ = ["Task"] | ||||
class Task: | class Task: | ||||
""" | """ | ||||
**EarwigBot: Base Bot Task** | **EarwigBot: Base Bot Task** | ||||
@@ -39,6 +37,7 @@ class Task: | |||||
<earwigbot.managers.TaskManager.start>`. ``**kwargs`` get passed to the | <earwigbot.managers.TaskManager.start>`. ``**kwargs`` get passed to the | ||||
Task's :meth:`run` method. | Task's :meth:`run` method. | ||||
""" | """ | ||||
name = None | name = None | ||||
number = 0 | number = 0 | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -167,7 +165,7 @@ class WikiProjectTagger(Task): | |||||
self.process_category(site.get_page(title), job, recursive) | self.process_category(site.get_page(title), job, recursive) | ||||
if "file" in kwargs: | if "file" in kwargs: | ||||
with open(kwargs["file"], "r") as fileobj: | |||||
with open(kwargs["file"]) as fileobj: | |||||
for line in fileobj: | for line in fileobj: | ||||
if line.strip(): | if line.strip(): | ||||
if line.startswith("[[") and line.endswith("]]"): | if line.startswith("[[") and line.endswith("]]"): | ||||
@@ -344,9 +342,9 @@ class WikiProjectTagger(Task): | |||||
def update_banner(self, banner, job, code): | def update_banner(self, banner, job, code): | ||||
"""Update an existing *banner* based on a *job* and a page's *code*.""" | """Update an existing *banner* based on a *job* and a page's *code*.""" | ||||
has = lambda key: ( | |||||
banner.has(key) and banner.get(key).value.strip() not in ("", "?") | |||||
) | |||||
def has(key): | |||||
return banner.has(key) and banner.get(key).value.strip() not in ("", "?") | |||||
updated = False | updated = False | ||||
if job.autoassess is not False: | if job.autoassess is not False: | ||||
@@ -1,6 +1,4 @@ | |||||
#! /usr/bin/env python | #! /usr/bin/env python | ||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -49,8 +47,8 @@ or run specific tasks. | |||||
""" | """ | ||||
from argparse import Action, ArgumentParser, REMAINDER | |||||
import logging | import logging | ||||
from argparse import REMAINDER, Action, ArgumentParser | |||||
from os import path | from os import path | ||||
from time import sleep | from time import sleep | ||||
@@ -59,8 +57,10 @@ from earwigbot.bot import Bot | |||||
__all__ = ["main"] | __all__ = ["main"] | ||||
class _StoreTaskArg(Action): | class _StoreTaskArg(Action): | ||||
"""A custom argparse action to read remaining command-line arguments.""" | """A custom argparse action to read remaining command-line arguments.""" | ||||
def __call__(self, parser, namespace, values, option_string=None): | def __call__(self, parser, namespace, values, option_string=None): | ||||
kwargs = {} | kwargs = {} | ||||
name = None | name = None | ||||
@@ -96,32 +96,53 @@ class _StoreTaskArg(Action): | |||||
def main(): | def main(): | ||||
"""Main entry point for the command-line utility.""" | """Main entry point for the command-line utility.""" | ||||
version = "EarwigBot v{0}".format(__version__) | |||||
version = f"EarwigBot v{__version__}" | |||||
desc = """This is EarwigBot's command-line utility, enabling you to easily | desc = """This is EarwigBot's command-line utility, enabling you to easily | ||||
start the bot or run specific tasks.""" | start the bot or run specific tasks.""" | ||||
parser = ArgumentParser(description=desc) | parser = ArgumentParser(description=desc) | ||||
parser.add_argument("path", nargs="?", metavar="PATH", default=path.curdir, | |||||
help="""path to the bot's working directory, which will | |||||
parser.add_argument( | |||||
"path", | |||||
nargs="?", | |||||
metavar="PATH", | |||||
default=path.curdir, | |||||
help="""path to the bot's working directory, which will | |||||
be created if it doesn't exist; current | be created if it doesn't exist; current | ||||
directory assumed if not specified""") | |||||
directory assumed if not specified""", | |||||
) | |||||
parser.add_argument("-v", "--version", action="version", version=version) | parser.add_argument("-v", "--version", action="version", version=version) | ||||
logger = parser.add_mutually_exclusive_group() | logger = parser.add_mutually_exclusive_group() | ||||
logger.add_argument("-d", "--debug", action="store_true", | |||||
help="print all logs, including DEBUG-level messages") | |||||
logger.add_argument("-q", "--quiet", action="store_true", | |||||
help="don't print any logs except warnings and errors") | |||||
parser.add_argument("-t", "--task", metavar="NAME", | |||||
help="""given the name of a task, the bot will run it | |||||
instead of the main bot and then exit""") | |||||
parser.add_argument("task_args", nargs=REMAINDER, action=_StoreTaskArg, | |||||
metavar="TASK_ARGS", | |||||
help="""with --task, will pass these arguments to the | |||||
task's run() method""") | |||||
logger.add_argument( | |||||
"-d", | |||||
"--debug", | |||||
action="store_true", | |||||
help="print all logs, including DEBUG-level messages", | |||||
) | |||||
logger.add_argument( | |||||
"-q", | |||||
"--quiet", | |||||
action="store_true", | |||||
help="don't print any logs except warnings and errors", | |||||
) | |||||
parser.add_argument( | |||||
"-t", | |||||
"--task", | |||||
metavar="NAME", | |||||
help="""given the name of a task, the bot will run it | |||||
instead of the main bot and then exit""", | |||||
) | |||||
parser.add_argument( | |||||
"task_args", | |||||
nargs=REMAINDER, | |||||
action=_StoreTaskArg, | |||||
metavar="TASK_ARGS", | |||||
help="""with --task, will pass these arguments to the | |||||
task's run() method""", | |||||
) | |||||
args = parser.parse_args() | args = parser.parse_args() | ||||
if not args.task and args.task_args: | if not args.task and args.task_args: | ||||
unrecognized = " ".join(args.task_args) | unrecognized = " ".join(args.task_args) | ||||
parser.error("unrecognized arguments: {0}".format(unrecognized)) | |||||
parser.error(f"unrecognized arguments: {unrecognized}") | |||||
level = logging.INFO | level = logging.INFO | ||||
if args.debug: | if args.debug: | ||||
@@ -153,5 +174,6 @@ def main(): | |||||
if bot.is_running: | if bot.is_running: | ||||
bot.stop() | bot.stop() | ||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
main() | main() |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -24,6 +22,7 @@ from earwigbot.wiki.page import Page | |||||
__all__ = ["Category"] | __all__ = ["Category"] | ||||
class Category(Page): | class Category(Page): | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Category** | **EarwigBot: Wiki Toolset: Category** | ||||
@@ -56,7 +55,7 @@ class Category(Page): | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Category.""" | """Return a nice string representation of the Category.""" | ||||
return '<Category "{0}" of {1}>'.format(self.title, str(self.site)) | |||||
return f'<Category "{self.title}" of {str(self.site)}>' | |||||
def __iter__(self): | def __iter__(self): | ||||
"""Iterate over all members of the category.""" | """Iterate over all members of the category.""" | ||||
@@ -64,8 +63,12 @@ class Category(Page): | |||||
def _get_members_via_api(self, limit, follow): | def _get_members_via_api(self, limit, follow): | ||||
"""Iterate over Pages in the category using the API.""" | """Iterate over Pages in the category using the API.""" | ||||
params = {"action": "query", "list": "categorymembers", | |||||
"cmtitle": self.title, "continue": ""} | |||||
params = { | |||||
"action": "query", | |||||
"list": "categorymembers", | |||||
"cmtitle": self.title, | |||||
"continue": "", | |||||
} | |||||
while 1: | while 1: | ||||
params["cmlimit"] = limit if limit else "max" | params["cmlimit"] = limit if limit else "max" | ||||
@@ -102,13 +105,13 @@ class Category(Page): | |||||
title = ":".join((namespace, base)) | title = ":".join((namespace, base)) | ||||
else: # Avoid doing a silly (albeit valid) ":Pagename" thing | else: # Avoid doing a silly (albeit valid) ":Pagename" thing | ||||
title = base | title = base | ||||
yield self.site.get_page(title, follow_redirects=follow, | |||||
pageid=row[2]) | |||||
yield self.site.get_page(title, follow_redirects=follow, pageid=row[2]) | |||||
def _get_size_via_api(self, member_type): | def _get_size_via_api(self, member_type): | ||||
"""Return the size of the category using the API.""" | """Return the size of the category using the API.""" | ||||
result = self.site.api_query(action="query", prop="categoryinfo", | |||||
titles=self.title) | |||||
result = self.site.api_query( | |||||
action="query", prop="categoryinfo", titles=self.title | |||||
) | |||||
info = list(result["query"]["pages"].values())[0]["categoryinfo"] | info = list(result["query"]["pages"].values())[0]["categoryinfo"] | ||||
return info[member_type] | return info[member_type] | ||||
@@ -127,7 +130,7 @@ class Category(Page): | |||||
"""Return the size of the category.""" | """Return the size of the category.""" | ||||
services = { | services = { | ||||
self.site.SERVICE_API: self._get_size_via_api, | self.site.SERVICE_API: self._get_size_via_api, | ||||
self.site.SERVICE_SQL: self._get_size_via_sql | |||||
self.site.SERVICE_SQL: self._get_size_via_sql, | |||||
} | } | ||||
return self.site.delegate(services, (member_type,)) | return self.site.delegate(services, (member_type,)) | ||||
@@ -201,7 +204,7 @@ class Category(Page): | |||||
""" | """ | ||||
services = { | services = { | ||||
self.site.SERVICE_API: self._get_members_via_api, | self.site.SERVICE_API: self._get_members_via_api, | ||||
self.site.SERVICE_SQL: self._get_members_via_sql | |||||
self.site.SERVICE_SQL: self._get_members_via_sql, | |||||
} | } | ||||
if follow_redirects is None: | if follow_redirects is None: | ||||
follow_redirects = self._follow_redirects | follow_redirects = self._follow_redirects | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -34,8 +32,10 @@ Import directly with ``from earwigbot.wiki import constants`` or | |||||
""" | """ | ||||
# Default User Agent when making API queries: | # Default User Agent when making API queries: | ||||
from earwigbot import __version__ as _v | |||||
from platform import python_version as _p | from platform import python_version as _p | ||||
from earwigbot import __version__ as _v | |||||
USER_AGENT = "EarwigBot/{0} (Python/{1}; https://github.com/earwig/earwigbot)" | USER_AGENT = "EarwigBot/{0} (Python/{1}; https://github.com/earwig/earwigbot)" | ||||
USER_AGENT = USER_AGENT.format(_v, _p()) | USER_AGENT = USER_AGENT.format(_v, _p()) | ||||
del _v, _p | del _v, _p | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,18 +18,18 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from time import sleep, time | |||||
from time import sleep | |||||
from urllib.request import build_opener | from urllib.request import build_opener | ||||
from earwigbot import exceptions | from earwigbot import exceptions | ||||
from earwigbot.wiki.copyvios.markov import MarkovChain | from earwigbot.wiki.copyvios.markov import MarkovChain | ||||
from earwigbot.wiki.copyvios.parsers import ArticleTextParser | from earwigbot.wiki.copyvios.parsers import ArticleTextParser | ||||
from earwigbot.wiki.copyvios.search import SEARCH_ENGINES | from earwigbot.wiki.copyvios.search import SEARCH_ENGINES | ||||
from earwigbot.wiki.copyvios.workers import ( | |||||
globalize, localize, CopyvioWorkspace) | |||||
from earwigbot.wiki.copyvios.workers import CopyvioWorkspace, globalize, localize | |||||
__all__ = ["CopyvioMixIn", "globalize", "localize"] | __all__ = ["CopyvioMixIn", "globalize", "localize"] | ||||
class CopyvioMixIn: | class CopyvioMixIn: | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Copyright Violation MixIn** | **EarwigBot: Wiki Toolset: Copyright Violation MixIn** | ||||
@@ -46,8 +44,10 @@ class CopyvioMixIn: | |||||
def __init__(self, site): | def __init__(self, site): | ||||
self._search_config = site._search_config | self._search_config = site._search_config | ||||
self._exclusions_db = self._search_config.get("exclusions_db") | self._exclusions_db = self._search_config.get("exclusions_db") | ||||
self._addheaders = [("User-Agent", site.user_agent), | |||||
("Accept-Encoding", "gzip")] | |||||
self._addheaders = [ | |||||
("User-Agent", site.user_agent), | |||||
("Accept-Encoding", "gzip"), | |||||
] | |||||
def _get_search_engine(self): | def _get_search_engine(self): | ||||
"""Return a function that can be called to do web searches. | """Return a function that can be called to do web searches. | ||||
@@ -80,8 +80,15 @@ class CopyvioMixIn: | |||||
return klass(credentials, opener) | return klass(credentials, opener) | ||||
def copyvio_check(self, min_confidence=0.75, max_queries=15, max_time=-1, | |||||
no_searches=False, no_links=False, short_circuit=True): | |||||
def copyvio_check( | |||||
self, | |||||
min_confidence=0.75, | |||||
max_queries=15, | |||||
max_time=-1, | |||||
no_searches=False, | |||||
no_links=False, | |||||
short_circuit=True, | |||||
): | |||||
"""Check the page for copyright violations. | """Check the page for copyright violations. | ||||
Returns a :class:`.CopyvioCheckResult` object with information on the | Returns a :class:`.CopyvioCheckResult` object with information on the | ||||
@@ -117,25 +124,34 @@ class CopyvioMixIn: | |||||
log = "Starting copyvio check for [[{0}]]" | log = "Starting copyvio check for [[{0}]]" | ||||
self._logger.info(log.format(self.title)) | self._logger.info(log.format(self.title)) | ||||
searcher = self._get_search_engine() | searcher = self._get_search_engine() | ||||
parser = ArticleTextParser(self.get(), args={ | |||||
"nltk_dir": self._search_config["nltk_dir"], | |||||
"lang": self._site.lang | |||||
}) | |||||
parser = ArticleTextParser( | |||||
self.get(), | |||||
args={"nltk_dir": self._search_config["nltk_dir"], "lang": self._site.lang}, | |||||
) | |||||
article = MarkovChain(parser.strip()) | article = MarkovChain(parser.strip()) | ||||
parser_args = {} | parser_args = {} | ||||
if self._exclusions_db: | if self._exclusions_db: | ||||
self._exclusions_db.sync(self.site.name) | self._exclusions_db.sync(self.site.name) | ||||
exclude = lambda u: self._exclusions_db.check(self.site.name, u) | |||||
parser_args["mirror_hints"] = \ | |||||
self._exclusions_db.get_mirror_hints(self) | |||||
def exclude(u): | |||||
return self._exclusions_db.check(self.site.name, u) | |||||
parser_args["mirror_hints"] = self._exclusions_db.get_mirror_hints(self) | |||||
else: | else: | ||||
exclude = None | exclude = None | ||||
workspace = CopyvioWorkspace( | workspace = CopyvioWorkspace( | ||||
article, min_confidence, max_time, self._logger, self._addheaders, | |||||
short_circuit=short_circuit, parser_args=parser_args, exclude_check=exclude, | |||||
config=self._search_config) | |||||
article, | |||||
min_confidence, | |||||
max_time, | |||||
self._logger, | |||||
self._addheaders, | |||||
short_circuit=short_circuit, | |||||
parser_args=parser_args, | |||||
exclude_check=exclude, | |||||
config=self._search_config, | |||||
) | |||||
if article.size < 20: # Auto-fail very small articles | if article.size < 20: # Auto-fail very small articles | ||||
result = workspace.get_result() | result = workspace.get_result() | ||||
@@ -187,8 +203,15 @@ class CopyvioMixIn: | |||||
self._logger.info(log.format(self.title, url)) | self._logger.info(log.format(self.title, url)) | ||||
article = MarkovChain(ArticleTextParser(self.get()).strip()) | article = MarkovChain(ArticleTextParser(self.get()).strip()) | ||||
workspace = CopyvioWorkspace( | workspace = CopyvioWorkspace( | ||||
article, min_confidence, max_time, self._logger, self._addheaders, | |||||
max_time, num_workers=1, config=self._search_config) | |||||
article, | |||||
min_confidence, | |||||
max_time, | |||||
self._logger, | |||||
self._addheaders, | |||||
max_time, | |||||
num_workers=1, | |||||
config=self._search_config, | |||||
) | |||||
workspace.enqueue([url]) | workspace.enqueue([url]) | ||||
workspace.wait() | workspace.wait() | ||||
result = workspace.get_result() | result = workspace.get_result() | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -33,18 +31,23 @@ __all__ = ["ExclusionsDB"] | |||||
DEFAULT_SOURCES = { | DEFAULT_SOURCES = { | ||||
"all": [ # Applies to all, but located on enwiki | "all": [ # Applies to all, but located on enwiki | ||||
"User:EarwigBot/Copyvios/Exclusions", | "User:EarwigBot/Copyvios/Exclusions", | ||||
"User:EranBot/Copyright/Blacklist" | |||||
"User:EranBot/Copyright/Blacklist", | |||||
], | ], | ||||
"enwiki": [ | "enwiki": [ | ||||
"Wikipedia:Mirrors and forks/ABC", "Wikipedia:Mirrors and forks/DEF", | |||||
"Wikipedia:Mirrors and forks/GHI", "Wikipedia:Mirrors and forks/JKL", | |||||
"Wikipedia:Mirrors and forks/MNO", "Wikipedia:Mirrors and forks/PQR", | |||||
"Wikipedia:Mirrors and forks/STU", "Wikipedia:Mirrors and forks/VWXYZ" | |||||
] | |||||
"Wikipedia:Mirrors and forks/ABC", | |||||
"Wikipedia:Mirrors and forks/DEF", | |||||
"Wikipedia:Mirrors and forks/GHI", | |||||
"Wikipedia:Mirrors and forks/JKL", | |||||
"Wikipedia:Mirrors and forks/MNO", | |||||
"Wikipedia:Mirrors and forks/PQR", | |||||
"Wikipedia:Mirrors and forks/STU", | |||||
"Wikipedia:Mirrors and forks/VWXYZ", | |||||
], | |||||
} | } | ||||
_RE_STRIP_PREFIX = r"^https?://(www\.)?" | _RE_STRIP_PREFIX = r"^https?://(www\.)?" | ||||
class ExclusionsDB: | class ExclusionsDB: | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Exclusions Database Manager** | **EarwigBot: Wiki Toolset: Exclusions Database Manager** | ||||
@@ -66,7 +69,7 @@ class ExclusionsDB: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the ExclusionsDB.""" | """Return a nice string representation of the ExclusionsDB.""" | ||||
return "<ExclusionsDB at {0}>".format(self._dbfile) | |||||
return f"<ExclusionsDB at {self._dbfile}>" | |||||
def _create(self): | def _create(self): | ||||
"""Initialize the exclusions database with its necessary tables.""" | """Initialize the exclusions database with its necessary tables.""" | ||||
@@ -95,7 +98,10 @@ class ExclusionsDB: | |||||
if source == "User:EarwigBot/Copyvios/Exclusions": | if source == "User:EarwigBot/Copyvios/Exclusions": | ||||
for line in data.splitlines(): | for line in data.splitlines(): | ||||
match = re.match(r"^\s*url\s*=\s*(?:\<nowiki\>\s*)?(.+?)\s*(?:\</nowiki\>\s*)?(?:#.*?)?$", line) | |||||
match = re.match( | |||||
r"^\s*url\s*=\s*(?:\<nowiki\>\s*)?(.+?)\s*(?:\</nowiki\>\s*)?(?:#.*?)?$", | |||||
line, | |||||
) | |||||
if match: | if match: | ||||
url = re.sub(_RE_STRIP_PREFIX, "", match.group(1)) | url = re.sub(_RE_STRIP_PREFIX, "", match.group(1)) | ||||
if url: | if url: | ||||
@@ -121,7 +127,9 @@ class ExclusionsDB: | |||||
"""Update the database from listed sources in the index.""" | """Update the database from listed sources in the index.""" | ||||
query1 = "SELECT source_page FROM sources WHERE source_sitename = ?" | query1 = "SELECT source_page FROM sources WHERE source_sitename = ?" | ||||
query2 = "SELECT exclusion_url FROM exclusions WHERE exclusion_sitename = ?" | query2 = "SELECT exclusion_url FROM exclusions WHERE exclusion_sitename = ?" | ||||
query3 = "DELETE FROM exclusions WHERE exclusion_sitename = ? AND exclusion_url = ?" | |||||
query3 = ( | |||||
"DELETE FROM exclusions WHERE exclusion_sitename = ? AND exclusion_url = ?" | |||||
) | |||||
query4 = "INSERT INTO exclusions VALUES (?, ?)" | query4 = "INSERT INTO exclusions VALUES (?, ?)" | ||||
query5 = "SELECT 1 FROM updates WHERE update_sitename = ?" | query5 = "SELECT 1 FROM updates WHERE update_sitename = ?" | ||||
query6 = "UPDATE updates SET update_time = ? WHERE update_sitename = ?" | query6 = "UPDATE updates SET update_time = ? WHERE update_sitename = ?" | ||||
@@ -206,7 +214,7 @@ class ExclusionsDB: | |||||
self._logger.debug(log.format(sitename, url)) | self._logger.debug(log.format(sitename, url)) | ||||
return True | return True | ||||
log = "No exclusions in {0} for {1}".format(sitename, url) | |||||
log = f"No exclusions in {sitename} for {url}" | |||||
self._logger.debug(log) | self._logger.debug(log) | ||||
return False | return False | ||||
@@ -224,9 +232,12 @@ class ExclusionsDB: | |||||
if try_mobile: | if try_mobile: | ||||
fragments = re.search(r"^([\w]+)\.([\w]+).([\w]+)$", site.domain) | fragments = re.search(r"^([\w]+)\.([\w]+).([\w]+)$", site.domain) | ||||
if fragments: | if fragments: | ||||
roots.append("{0}.m.{1}.{2}".format(*fragments.groups())) | |||||
roots.append("{}.m.{}.{}".format(*fragments.groups())) | |||||
general = [root + site._script_path + "/" + script | |||||
for root in roots for script in scripts] | |||||
general = [ | |||||
root + site._script_path + "/" + script | |||||
for root in roots | |||||
for script in scripts | |||||
] | |||||
specific = [root + path for root in roots] | specific = [root + path for root in roots] | ||||
return general + specific | return general + specific |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,13 +18,14 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from re import sub, UNICODE | |||||
from re import UNICODE, sub | |||||
__all__ = ["EMPTY", "EMPTY_INTERSECTION", "MarkovChain", "MarkovChainIntersection"] | |||||
__all__ = ["EMPTY", "EMPTY_INTERSECTION", "MarkovChain", | |||||
"MarkovChainIntersection"] | |||||
class MarkovChain: | class MarkovChain: | ||||
"""Implements a basic ngram Markov chain of words.""" | """Implements a basic ngram Markov chain of words.""" | ||||
START = -1 | START = -1 | ||||
END = -2 | END = -2 | ||||
degree = 5 # 2 for bigrams, 3 for trigrams, etc. | degree = 5 # 2 for bigrams, 3 for trigrams, etc. | ||||
@@ -44,7 +43,7 @@ class MarkovChain: | |||||
chain = {} | chain = {} | ||||
for i in range(len(words) - self.degree + 1): | for i in range(len(words) - self.degree + 1): | ||||
phrase = tuple(words[i:i+self.degree]) | |||||
phrase = tuple(words[i : i + self.degree]) | |||||
if phrase in chain: | if phrase in chain: | ||||
chain[phrase] += 1 | chain[phrase] += 1 | ||||
else: | else: | ||||
@@ -57,11 +56,11 @@ class MarkovChain: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the MarkovChain.""" | """Return the canonical string representation of the MarkovChain.""" | ||||
return "MarkovChain(text={0!r})".format(self.text) | |||||
return f"MarkovChain(text={self.text!r})" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the MarkovChain.""" | """Return a nice string representation of the MarkovChain.""" | ||||
return "<MarkovChain of size {0}>".format(self.size) | |||||
return f"<MarkovChain of size {self.size}>" | |||||
class MarkovChainIntersection(MarkovChain): | class MarkovChainIntersection(MarkovChain): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,11 +19,11 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
import json | import json | ||||
from os import path | |||||
import re | import re | ||||
from io import StringIO | |||||
import urllib.parse | import urllib.parse | ||||
import urllib.request | import urllib.request | ||||
from io import StringIO | |||||
from os import path | |||||
import mwparserfromhell | import mwparserfromhell | ||||
@@ -40,8 +38,10 @@ pdfpage = importer.new("pdfminer.pdfpage") | |||||
__all__ = ["ArticleTextParser", "get_parser"] | __all__ = ["ArticleTextParser", "get_parser"] | ||||
class _BaseTextParser: | class _BaseTextParser: | ||||
"""Base class for a parser that handles text.""" | """Base class for a parser that handles text.""" | ||||
TYPE = None | TYPE = None | ||||
def __init__(self, text, url=None, args=None): | def __init__(self, text, url=None, args=None): | ||||
@@ -51,16 +51,17 @@ class _BaseTextParser: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the text parser.""" | """Return the canonical string representation of the text parser.""" | ||||
return "{0}(text={1!r})".format(self.__class__.__name__, self.text) | |||||
return f"{self.__class__.__name__}(text={self.text!r})" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the text parser.""" | """Return a nice string representation of the text parser.""" | ||||
name = self.__class__.__name__ | name = self.__class__.__name__ | ||||
return "<{0} of text with size {1}>".format(name, len(self.text)) | |||||
return f"<{name} of text with size {len(self.text)}>" | |||||
class ArticleTextParser(_BaseTextParser): | class ArticleTextParser(_BaseTextParser): | ||||
"""A parser that can strip and chunk wikicode article text.""" | """A parser that can strip and chunk wikicode article text.""" | ||||
TYPE = "Article" | TYPE = "Article" | ||||
TEMPLATE_MERGE_THRESHOLD = 35 | TEMPLATE_MERGE_THRESHOLD = 35 | ||||
NLTK_DEFAULT = "english" | NLTK_DEFAULT = "english" | ||||
@@ -81,7 +82,7 @@ class ArticleTextParser(_BaseTextParser): | |||||
"pt": "portuguese", | "pt": "portuguese", | ||||
"sl": "slovene", | "sl": "slovene", | ||||
"sv": "swedish", | "sv": "swedish", | ||||
"tr": "turkish" | |||||
"tr": "turkish", | |||||
} | } | ||||
def _merge_templates(self, code): | def _merge_templates(self, code): | ||||
@@ -100,8 +101,11 @@ class ArticleTextParser(_BaseTextParser): | |||||
def _get_tokenizer(self): | def _get_tokenizer(self): | ||||
"""Return a NLTK punctuation tokenizer for the article's language.""" | """Return a NLTK punctuation tokenizer for the article's language.""" | ||||
datafile = lambda lang: "file:" + path.join( | |||||
self._args["nltk_dir"], "tokenizers", "punkt", lang + ".pickle") | |||||
def datafile(lang): | |||||
return "file:" + path.join( | |||||
self._args["nltk_dir"], "tokenizers", "punkt", lang + ".pickle" | |||||
) | |||||
lang = self.NLTK_LANGS.get(self._args.get("lang"), self.NLTK_DEFAULT) | lang = self.NLTK_LANGS.get(self._args.get("lang"), self.NLTK_DEFAULT) | ||||
try: | try: | ||||
@@ -112,6 +116,7 @@ class ArticleTextParser(_BaseTextParser): | |||||
def _get_sentences(self, min_query, max_query, split_thresh): | def _get_sentences(self, min_query, max_query, split_thresh): | ||||
"""Split the article text into sentences of a certain length.""" | """Split the article text into sentences of a certain length.""" | ||||
def cut_sentence(words): | def cut_sentence(words): | ||||
div = len(words) | div = len(words) | ||||
if div == 0: | if div == 0: | ||||
@@ -125,7 +130,7 @@ class ArticleTextParser(_BaseTextParser): | |||||
result = [] | result = [] | ||||
if length >= split_thresh: | if length >= split_thresh: | ||||
result.append(" ".join(words[:div])) | result.append(" ".join(words[:div])) | ||||
return result + cut_sentence(words[div + 1:]) | |||||
return result + cut_sentence(words[div + 1 :]) | |||||
tokenizer = self._get_tokenizer() | tokenizer = self._get_tokenizer() | ||||
sentences = [] | sentences = [] | ||||
@@ -150,6 +155,7 @@ class ArticleTextParser(_BaseTextParser): | |||||
The actual stripping is handled by :py:mod:`mwparserfromhell`. | The actual stripping is handled by :py:mod:`mwparserfromhell`. | ||||
""" | """ | ||||
def remove(code, node): | def remove(code, node): | ||||
"""Remove a node from a code object, ignoring ValueError. | """Remove a node from a code object, ignoring ValueError. | ||||
@@ -223,16 +229,14 @@ class ArticleTextParser(_BaseTextParser): | |||||
""" | """ | ||||
schemes = ("http://", "https://") | schemes = ("http://", "https://") | ||||
links = mwparserfromhell.parse(self.text).ifilter_external_links() | links = mwparserfromhell.parse(self.text).ifilter_external_links() | ||||
return [str(link.url) for link in links | |||||
if link.url.startswith(schemes)] | |||||
return [str(link.url) for link in links if link.url.startswith(schemes)] | |||||
class _HTMLParser(_BaseTextParser): | class _HTMLParser(_BaseTextParser): | ||||
"""A parser that can extract the text from an HTML document.""" | """A parser that can extract the text from an HTML document.""" | ||||
TYPE = "HTML" | TYPE = "HTML" | ||||
hidden_tags = [ | |||||
"script", "style" | |||||
] | |||||
hidden_tags = ["script", "style"] | |||||
def _fail_if_mirror(self, soup): | def _fail_if_mirror(self, soup): | ||||
"""Look for obvious signs that the given soup is a wiki mirror. | """Look for obvious signs that the given soup is a wiki mirror. | ||||
@@ -243,8 +247,9 @@ class _HTMLParser(_BaseTextParser): | |||||
if "mirror_hints" not in self._args: | if "mirror_hints" not in self._args: | ||||
return | return | ||||
func = lambda attr: attr and any( | |||||
hint in attr for hint in self._args["mirror_hints"]) | |||||
def func(attr): | |||||
return attr and any(hint in attr for hint in self._args["mirror_hints"]) | |||||
if soup.find_all(href=func) or soup.find_all(src=func): | if soup.find_all(href=func) or soup.find_all(src=func): | ||||
raise ParserExclusionError() | raise ParserExclusionError() | ||||
@@ -258,7 +263,10 @@ class _HTMLParser(_BaseTextParser): | |||||
def _clean_soup(self, soup): | def _clean_soup(self, soup): | ||||
"""Clean a BeautifulSoup tree of invisible tags.""" | """Clean a BeautifulSoup tree of invisible tags.""" | ||||
is_comment = lambda text: isinstance(text, bs4.element.Comment) | |||||
def is_comment(text): | |||||
return isinstance(text, bs4.element.Comment) | |||||
for comment in soup.find_all(text=is_comment): | for comment in soup.find_all(text=is_comment): | ||||
comment.extract() | comment.extract() | ||||
for tag in self.hidden_tags: | for tag in self.hidden_tags: | ||||
@@ -281,15 +289,17 @@ class _HTMLParser(_BaseTextParser): | |||||
if not match: | if not match: | ||||
return "" | return "" | ||||
post_id = match.group(1) | post_id = match.group(1) | ||||
url = "https://%s/feeds/posts/default/%s?" % (url.netloc, post_id) | |||||
url = f"https://{url.netloc}/feeds/posts/default/{post_id}?" | |||||
params = { | params = { | ||||
"alt": "json", | "alt": "json", | ||||
"v": "2", | "v": "2", | ||||
"dynamicviews": "1", | "dynamicviews": "1", | ||||
"rewriteforssl": "true", | "rewriteforssl": "true", | ||||
} | } | ||||
raw = self._open(url + urllib.parse.urlencode(params), | |||||
allow_content_types=["application/json"]) | |||||
raw = self._open( | |||||
url + urllib.parse.urlencode(params), | |||||
allow_content_types=["application/json"], | |||||
) | |||||
if raw is None: | if raw is None: | ||||
return "" | return "" | ||||
try: | try: | ||||
@@ -334,6 +344,7 @@ class _HTMLParser(_BaseTextParser): | |||||
class _PDFParser(_BaseTextParser): | class _PDFParser(_BaseTextParser): | ||||
"""A parser that can extract text from a PDF file.""" | """A parser that can extract text from a PDF file.""" | ||||
TYPE = "PDF" | TYPE = "PDF" | ||||
substitutions = [ | substitutions = [ | ||||
("\x0c", "\n"), | ("\x0c", "\n"), | ||||
@@ -364,6 +375,7 @@ class _PDFParser(_BaseTextParser): | |||||
class _PlainTextParser(_BaseTextParser): | class _PlainTextParser(_BaseTextParser): | ||||
"""A parser that can unicode-ify and strip text from a plain text page.""" | """A parser that can unicode-ify and strip text from a plain text page.""" | ||||
TYPE = "Text" | TYPE = "Text" | ||||
def parse(self): | def parse(self): | ||||
@@ -377,9 +389,10 @@ _CONTENT_TYPES = { | |||||
"application/xhtml+xml": _HTMLParser, | "application/xhtml+xml": _HTMLParser, | ||||
"application/pdf": _PDFParser, | "application/pdf": _PDFParser, | ||||
"application/x-pdf": _PDFParser, | "application/x-pdf": _PDFParser, | ||||
"text/plain": _PlainTextParser | |||||
"text/plain": _PlainTextParser, | |||||
} | } | ||||
def get_parser(content_type): | def get_parser(content_type): | ||||
"""Return the parser most able to handle a given content type, or None.""" | """Return the parser most able to handle a given content type, or None.""" | ||||
return _CONTENT_TYPES.get(content_type) | return _CONTENT_TYPES.get(content_type) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -27,6 +25,7 @@ from earwigbot.wiki.copyvios.markov import EMPTY, EMPTY_INTERSECTION | |||||
__all__ = ["CopyvioSource", "CopyvioCheckResult"] | __all__ = ["CopyvioSource", "CopyvioCheckResult"] | ||||
class CopyvioSource: | class CopyvioSource: | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Copyvio Source** | **EarwigBot: Wiki Toolset: Copyvio Source** | ||||
@@ -43,8 +42,15 @@ class CopyvioSource: | |||||
- :py:attr:`excluded`: whether this URL was in the exclusions list | - :py:attr:`excluded`: whether this URL was in the exclusions list | ||||
""" | """ | ||||
def __init__(self, workspace, url, headers=None, timeout=5, | |||||
parser_args=None, search_config=None): | |||||
def __init__( | |||||
self, | |||||
workspace, | |||||
url, | |||||
headers=None, | |||||
timeout=5, | |||||
parser_args=None, | |||||
search_config=None, | |||||
): | |||||
self.workspace = workspace | self.workspace = workspace | ||||
self.url = url | self.url = url | ||||
self.headers = headers | self.headers = headers | ||||
@@ -63,17 +69,18 @@ class CopyvioSource: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the source.""" | """Return the canonical string representation of the source.""" | ||||
res = ("CopyvioSource(url={0!r}, confidence={1!r}, skipped={2!r}, " | |||||
"excluded={3!r})") | |||||
return res.format( | |||||
self.url, self.confidence, self.skipped, self.excluded) | |||||
res = ( | |||||
"CopyvioSource(url={0!r}, confidence={1!r}, skipped={2!r}, " | |||||
"excluded={3!r})" | |||||
) | |||||
return res.format(self.url, self.confidence, self.skipped, self.excluded) | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the source.""" | """Return a nice string representation of the source.""" | ||||
if self.excluded: | if self.excluded: | ||||
return "<CopyvioSource ({0}, excluded)>".format(self.url) | |||||
return f"<CopyvioSource ({self.url}, excluded)>" | |||||
if self.skipped: | if self.skipped: | ||||
return "<CopyvioSource ({0}, skipped)>".format(self.url) | |||||
return f"<CopyvioSource ({self.url}, skipped)>" | |||||
res = "<CopyvioSource ({0} with {1} conf)>" | res = "<CopyvioSource ({0} with {1} conf)>" | ||||
return res.format(self.url, self.confidence) | return res.format(self.url, self.confidence) | ||||
@@ -129,8 +136,9 @@ class CopyvioCheckResult: | |||||
- :py:attr:`possible_miss`: whether some URLs might have been missed | - :py:attr:`possible_miss`: whether some URLs might have been missed | ||||
""" | """ | ||||
def __init__(self, violation, sources, queries, check_time, article_chain, | |||||
possible_miss): | |||||
def __init__( | |||||
self, violation, sources, queries, check_time, article_chain, possible_miss | |||||
): | |||||
self.violation = violation | self.violation = violation | ||||
self.sources = sources | self.sources = sources | ||||
self.queries = queries | self.queries = queries | ||||
@@ -141,8 +149,7 @@ class CopyvioCheckResult: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the result.""" | """Return the canonical string representation of the result.""" | ||||
res = "CopyvioCheckResult(violation={0!r}, sources={1!r}, queries={2!r}, time={3!r})" | res = "CopyvioCheckResult(violation={0!r}, sources={1!r}, queries={2!r}, time={3!r})" | ||||
return res.format(self.violation, self.sources, self.queries, | |||||
self.time) | |||||
return res.format(self.violation, self.sources, self.queries, self.time) | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the result.""" | """Return a nice string representation of the result.""" | ||||
@@ -171,5 +178,12 @@ class CopyvioCheckResult: | |||||
return log.format(title, self.queries, self.time) | return log.format(title, self.queries, self.time) | ||||
log = "{0} for [[{1}]] (best: {2} ({3} confidence); {4} sources; {5} queries; {6} seconds)" | log = "{0} for [[{1}]] (best: {2} ({3} confidence); {4} sources; {5} queries; {6} seconds)" | ||||
is_vio = "Violation detected" if self.violation else "No violation" | is_vio = "Violation detected" if self.violation else "No violation" | ||||
return log.format(is_vio, title, self.url, self.confidence, | |||||
len(self.sources), self.queries, self.time) | |||||
return log.format( | |||||
is_vio, | |||||
title, | |||||
self.url, | |||||
self.confidence, | |||||
len(self.sources), | |||||
self.queries, | |||||
self.time, | |||||
) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,22 +19,28 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
from gzip import GzipFile | from gzip import GzipFile | ||||
from io import StringIO | |||||
from json import loads | from json import loads | ||||
from re import sub as re_sub | from re import sub as re_sub | ||||
from socket import error | |||||
from io import StringIO | |||||
from urllib.parse import quote, urlencode | |||||
from urllib.error import URLError | from urllib.error import URLError | ||||
from urllib.parse import urlencode | |||||
from earwigbot import importer | from earwigbot import importer | ||||
from earwigbot.exceptions import SearchQueryError | from earwigbot.exceptions import SearchQueryError | ||||
lxml = importer.new("lxml") | lxml = importer.new("lxml") | ||||
__all__ = ["BingSearchEngine", "GoogleSearchEngine", "YandexSearchEngine", "SEARCH_ENGINES"] | |||||
__all__ = [ | |||||
"BingSearchEngine", | |||||
"GoogleSearchEngine", | |||||
"YandexSearchEngine", | |||||
"SEARCH_ENGINES", | |||||
] | |||||
class _BaseSearchEngine: | class _BaseSearchEngine: | ||||
"""Base class for a simple search engine interface.""" | """Base class for a simple search engine interface.""" | ||||
name = "Base" | name = "Base" | ||||
def __init__(self, cred, opener): | def __init__(self, cred, opener): | ||||
@@ -47,19 +51,19 @@ class _BaseSearchEngine: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the search engine.""" | """Return the canonical string representation of the search engine.""" | ||||
return "{0}()".format(self.__class__.__name__) | |||||
return f"{self.__class__.__name__}()" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the search engine.""" | """Return a nice string representation of the search engine.""" | ||||
return "<{0}>".format(self.__class__.__name__) | |||||
return f"<{self.__class__.__name__}>" | |||||
def _open(self, *args): | def _open(self, *args): | ||||
"""Open a URL (like urlopen) and try to return its contents.""" | """Open a URL (like urlopen) and try to return its contents.""" | ||||
try: | try: | ||||
response = self.opener.open(*args) | response = self.opener.open(*args) | ||||
result = response.read() | result = response.read() | ||||
except (URLError, error) as exc: | |||||
err = SearchQueryError("{0} Error: {1}".format(self.name, exc)) | |||||
except (OSError, URLError) as exc: | |||||
err = SearchQueryError(f"{self.name} Error: {exc}") | |||||
err.cause = exc | err.cause = exc | ||||
raise err | raise err | ||||
@@ -90,6 +94,7 @@ class _BaseSearchEngine: | |||||
class BingSearchEngine(_BaseSearchEngine): | class BingSearchEngine(_BaseSearchEngine): | ||||
"""A search engine interface with Bing Search (via Azure Marketplace).""" | """A search engine interface with Bing Search (via Azure Marketplace).""" | ||||
name = "Bing" | name = "Bing" | ||||
def __init__(self, cred, opener): | def __init__(self, cred, opener): | ||||
@@ -106,7 +111,7 @@ class BingSearchEngine(_BaseSearchEngine): | |||||
Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | ||||
""" | """ | ||||
service = "SearchWeb" if self.cred["type"] == "searchweb" else "Search" | service = "SearchWeb" if self.cred["type"] == "searchweb" else "Search" | ||||
url = "https://api.datamarket.azure.com/Bing/{0}/Web?".format(service) | |||||
url = f"https://api.datamarket.azure.com/Bing/{service}/Web?" | |||||
params = { | params = { | ||||
"$format": "json", | "$format": "json", | ||||
"$top": str(self.count), | "$top": str(self.count), | ||||
@@ -114,7 +119,7 @@ class BingSearchEngine(_BaseSearchEngine): | |||||
"Market": "'en-US'", | "Market": "'en-US'", | ||||
"Adult": "'Off'", | "Adult": "'Off'", | ||||
"Options": "'DisableLocationDetection'", | "Options": "'DisableLocationDetection'", | ||||
"WebSearchOptions": "'DisableHostCollapsing+DisableQueryAlterations'" | |||||
"WebSearchOptions": "'DisableHostCollapsing+DisableQueryAlterations'", | |||||
} | } | ||||
result = self._open(url + urlencode(params)) | result = self._open(url + urlencode(params)) | ||||
@@ -134,6 +139,7 @@ class BingSearchEngine(_BaseSearchEngine): | |||||
class GoogleSearchEngine(_BaseSearchEngine): | class GoogleSearchEngine(_BaseSearchEngine): | ||||
"""A search engine interface with Google Search.""" | """A search engine interface with Google Search.""" | ||||
name = "Google" | name = "Google" | ||||
def search(self, query): | def search(self, query): | ||||
@@ -143,7 +149,7 @@ class GoogleSearchEngine(_BaseSearchEngine): | |||||
Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | ||||
""" | """ | ||||
domain = self.cred.get("proxy", "www.googleapis.com") | domain = self.cred.get("proxy", "www.googleapis.com") | ||||
url = "https://{0}/customsearch/v1?".format(domain) | |||||
url = f"https://{domain}/customsearch/v1?" | |||||
params = { | params = { | ||||
"cx": self.cred["id"], | "cx": self.cred["id"], | ||||
"key": self.cred["key"], | "key": self.cred["key"], | ||||
@@ -151,7 +157,7 @@ class GoogleSearchEngine(_BaseSearchEngine): | |||||
"alt": "json", | "alt": "json", | ||||
"num": str(self.count), | "num": str(self.count), | ||||
"safe": "off", | "safe": "off", | ||||
"fields": "items(link)" | |||||
"fields": "items(link)", | |||||
} | } | ||||
result = self._open(url + urlencode(params)) | result = self._open(url + urlencode(params)) | ||||
@@ -170,6 +176,7 @@ class GoogleSearchEngine(_BaseSearchEngine): | |||||
class YandexSearchEngine(_BaseSearchEngine): | class YandexSearchEngine(_BaseSearchEngine): | ||||
"""A search engine interface with Yandex Search.""" | """A search engine interface with Yandex Search.""" | ||||
name = "Yandex" | name = "Yandex" | ||||
@staticmethod | @staticmethod | ||||
@@ -183,7 +190,7 @@ class YandexSearchEngine(_BaseSearchEngine): | |||||
Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | Raises :py:exc:`~earwigbot.exceptions.SearchQueryError` on errors. | ||||
""" | """ | ||||
domain = self.cred.get("proxy", "yandex.com") | domain = self.cred.get("proxy", "yandex.com") | ||||
url = "https://{0}/search/xml?".format(domain) | |||||
url = f"https://{domain}/search/xml?" | |||||
query = re_sub(r"[^a-zA-Z0-9 ]", "", query).encode("utf8") | query = re_sub(r"[^a-zA-Z0-9 ]", "", query).encode("utf8") | ||||
params = { | params = { | ||||
"user": self.cred["user"], | "user": self.cred["user"], | ||||
@@ -192,7 +199,7 @@ class YandexSearchEngine(_BaseSearchEngine): | |||||
"l10n": "en", | "l10n": "en", | ||||
"filter": "none", | "filter": "none", | ||||
"maxpassages": "1", | "maxpassages": "1", | ||||
"groupby": "mode=flat.groups-on-page={0}".format(self.count) | |||||
"groupby": f"mode=flat.groups-on-page={self.count}", | |||||
} | } | ||||
result = self._open(url + urlencode(params)) | result = self._open(url + urlencode(params)) | ||||
@@ -207,5 +214,5 @@ class YandexSearchEngine(_BaseSearchEngine): | |||||
SEARCH_ENGINES = { | SEARCH_ENGINES = { | ||||
"Bing": BingSearchEngine, | "Bing": BingSearchEngine, | ||||
"Google": GoogleSearchEngine, | "Google": GoogleSearchEngine, | ||||
"Yandex": YandexSearchEngine | |||||
"Yandex": YandexSearchEngine, | |||||
} | } |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,21 +20,20 @@ | |||||
import base64 | import base64 | ||||
import collections | import collections | ||||
from collections import deque | |||||
import functools | import functools | ||||
import time | |||||
import urllib.parse | |||||
from collections import deque | |||||
from gzip import GzipFile | from gzip import GzipFile | ||||
from http.client import HTTPException | from http.client import HTTPException | ||||
from io import StringIO | |||||
from logging import getLogger | from logging import getLogger | ||||
from math import log | from math import log | ||||
from queue import Empty, Queue | from queue import Empty, Queue | ||||
from socket import error as socket_error | |||||
from io import StringIO | |||||
from struct import error as struct_error | from struct import error as struct_error | ||||
from threading import Lock, Thread | from threading import Lock, Thread | ||||
import time | |||||
from urllib.error import URLError | from urllib.error import URLError | ||||
import urllib.parse | |||||
from urllib.request import build_opener, Request | |||||
from urllib.request import Request, build_opener | |||||
from earwigbot import importer | from earwigbot import importer | ||||
from earwigbot.exceptions import ParserExclusionError, ParserRedirectError | from earwigbot.exceptions import ParserExclusionError, ParserRedirectError | ||||
@@ -49,13 +46,14 @@ tldextract = importer.new("tldextract") | |||||
__all__ = ["globalize", "localize", "CopyvioWorkspace"] | __all__ = ["globalize", "localize", "CopyvioWorkspace"] | ||||
_MAX_REDIRECTS = 3 | _MAX_REDIRECTS = 3 | ||||
_MAX_RAW_SIZE = 20 * 1024 ** 2 | |||||
_MAX_RAW_SIZE = 20 * 1024**2 | |||||
_is_globalized = False | _is_globalized = False | ||||
_global_queues = None | _global_queues = None | ||||
_global_workers = [] | _global_workers = [] | ||||
_OpenedURL = collections.namedtuple('_OpenedURL', ['content', 'parser_class']) | |||||
_OpenedURL = collections.namedtuple("_OpenedURL", ["content", "parser_class"]) | |||||
def globalize(num_workers=8): | def globalize(num_workers=8): | ||||
"""Cause all copyvio checks to be done by one global set of workers. | """Cause all copyvio checks to be done by one global set of workers. | ||||
@@ -74,11 +72,12 @@ def globalize(num_workers=8): | |||||
_global_queues = _CopyvioQueues() | _global_queues = _CopyvioQueues() | ||||
for i in range(num_workers): | for i in range(num_workers): | ||||
worker = _CopyvioWorker("global-{0}".format(i), _global_queues) | |||||
worker = _CopyvioWorker(f"global-{i}", _global_queues) | |||||
worker.start() | worker.start() | ||||
_global_workers.append(worker) | _global_workers.append(worker) | ||||
_is_globalized = True | _is_globalized = True | ||||
def localize(): | def localize(): | ||||
"""Return to using page-specific workers for copyvio checks. | """Return to using page-specific workers for copyvio checks. | ||||
@@ -137,11 +136,12 @@ class _CopyvioWorker: | |||||
if "path" in proxy_info: | if "path" in proxy_info: | ||||
if not parsed.path.startswith(proxy_info["path"]): | if not parsed.path.startswith(proxy_info["path"]): | ||||
continue | continue | ||||
path = path[len(proxy_info["path"]):] | |||||
path = path[len(proxy_info["path"]) :] | |||||
url = proxy_info["target"] + path | url = proxy_info["target"] + path | ||||
if "auth" in proxy_info: | if "auth" in proxy_info: | ||||
extra_headers["Authorization"] = "Basic %s" % ( | extra_headers["Authorization"] = "Basic %s" % ( | ||||
base64.b64encode(proxy_info["auth"])) | |||||
base64.b64encode(proxy_info["auth"]) | |||||
) | |||||
return url, True | return url, True | ||||
return url, False | return url, False | ||||
@@ -158,8 +158,10 @@ class _CopyvioWorker: | |||||
request = Request(url, headers=extra_headers) | request = Request(url, headers=extra_headers) | ||||
try: | try: | ||||
response = self._opener.open(request, timeout=timeout) | response = self._opener.open(request, timeout=timeout) | ||||
except (URLError, HTTPException, socket_error, ValueError): | |||||
url, remapped = self._try_map_proxy_url(url, parsed, extra_headers, is_error=True) | |||||
except (OSError, URLError, HTTPException, ValueError): | |||||
url, remapped = self._try_map_proxy_url( | |||||
url, parsed, extra_headers, is_error=True | |||||
) | |||||
if not remapped: | if not remapped: | ||||
self._logger.exception("Failed to fetch URL: %s", url) | self._logger.exception("Failed to fetch URL: %s", url) | ||||
return None | return None | ||||
@@ -167,7 +169,7 @@ class _CopyvioWorker: | |||||
request = Request(url, headers=extra_headers) | request = Request(url, headers=extra_headers) | ||||
try: | try: | ||||
response = self._opener.open(request, timeout=timeout) | response = self._opener.open(request, timeout=timeout) | ||||
except (URLError, HTTPException, socket_error, ValueError): | |||||
except (OSError, URLError, HTTPException, ValueError): | |||||
self._logger.exception("Failed to fetch URL after proxy remap: %s", url) | self._logger.exception("Failed to fetch URL after proxy remap: %s", url) | ||||
return None | return None | ||||
@@ -180,18 +182,19 @@ class _CopyvioWorker: | |||||
content_type = content_type.split(";", 1)[0] | content_type = content_type.split(";", 1)[0] | ||||
parser_class = get_parser(content_type) | parser_class = get_parser(content_type) | ||||
if not parser_class and ( | if not parser_class and ( | ||||
not allow_content_types or content_type not in allow_content_types): | |||||
not allow_content_types or content_type not in allow_content_types | |||||
): | |||||
return None | return None | ||||
if not parser_class: | if not parser_class: | ||||
parser_class = get_parser("text/plain") | parser_class = get_parser("text/plain") | ||||
if size > (15 if parser_class.TYPE == "PDF" else 2) * 1024 ** 2: | |||||
if size > (15 if parser_class.TYPE == "PDF" else 2) * 1024**2: | |||||
return None | return None | ||||
try: | try: | ||||
# Additional safety check for pages using Transfer-Encoding: chunked | # Additional safety check for pages using Transfer-Encoding: chunked | ||||
# where we can't read the Content-Length | # where we can't read the Content-Length | ||||
content = response.read(_MAX_RAW_SIZE + 1) | content = response.read(_MAX_RAW_SIZE + 1) | ||||
except (URLError, socket_error): | |||||
except (OSError, URLError): | |||||
return None | return None | ||||
if len(content) > _MAX_RAW_SIZE: | if len(content) > _MAX_RAW_SIZE: | ||||
return None | return None | ||||
@@ -201,7 +204,7 @@ class _CopyvioWorker: | |||||
gzipper = GzipFile(fileobj=stream) | gzipper = GzipFile(fileobj=stream) | ||||
try: | try: | ||||
content = gzipper.read() | content = gzipper.read() | ||||
except (IOError, struct_error): | |||||
except (OSError, struct_error): | |||||
return None | return None | ||||
if len(content) > _MAX_RAW_SIZE: | if len(content) > _MAX_RAW_SIZE: | ||||
@@ -252,7 +255,7 @@ class _CopyvioWorker: | |||||
site, queue = self._queues.unassigned.get(timeout=timeout) | site, queue = self._queues.unassigned.get(timeout=timeout) | ||||
if site is StopIteration: | if site is StopIteration: | ||||
raise StopIteration | raise StopIteration | ||||
self._logger.debug("Acquired new site queue: {0}".format(site)) | |||||
self._logger.debug(f"Acquired new site queue: {site}") | |||||
self._site = site | self._site = site | ||||
self._queue = queue | self._queue = queue | ||||
@@ -274,7 +277,7 @@ class _CopyvioWorker: | |||||
self._queues.lock.release() | self._queues.lock.release() | ||||
return self._dequeue() | return self._dequeue() | ||||
self._logger.debug("Got source URL: {0}".format(source.url)) | |||||
self._logger.debug(f"Got source URL: {source.url}") | |||||
if source.skipped: | if source.skipped: | ||||
self._logger.debug("Source has been skipped") | self._logger.debug("Source has been skipped") | ||||
self._queues.lock.release() | self._queues.lock.release() | ||||
@@ -335,9 +338,20 @@ class _CopyvioWorker: | |||||
class CopyvioWorkspace: | class CopyvioWorkspace: | ||||
"""Manages a single copyvio check distributed across threads.""" | """Manages a single copyvio check distributed across threads.""" | ||||
def __init__(self, article, min_confidence, max_time, logger, headers, | |||||
url_timeout=5, num_workers=8, short_circuit=True, | |||||
parser_args=None, exclude_check=None, config=None): | |||||
def __init__( | |||||
self, | |||||
article, | |||||
min_confidence, | |||||
max_time, | |||||
logger, | |||||
headers, | |||||
url_timeout=5, | |||||
num_workers=8, | |||||
short_circuit=True, | |||||
parser_args=None, | |||||
exclude_check=None, | |||||
config=None, | |||||
): | |||||
self.sources = [] | self.sources = [] | ||||
self.finished = False | self.finished = False | ||||
self.possible_miss = False | self.possible_miss = False | ||||
@@ -351,8 +365,12 @@ class CopyvioWorkspace: | |||||
self._finish_lock = Lock() | self._finish_lock = Lock() | ||||
self._short_circuit = short_circuit | self._short_circuit = short_circuit | ||||
self._source_args = { | self._source_args = { | ||||
"workspace": self, "headers": headers, "timeout": url_timeout, | |||||
"parser_args": parser_args, "search_config": config} | |||||
"workspace": self, | |||||
"headers": headers, | |||||
"timeout": url_timeout, | |||||
"parser_args": parser_args, | |||||
"search_config": config, | |||||
} | |||||
self._exclude_check = exclude_check | self._exclude_check = exclude_check | ||||
if _is_globalized: | if _is_globalized: | ||||
@@ -361,11 +379,12 @@ class CopyvioWorkspace: | |||||
self._queues = _CopyvioQueues() | self._queues = _CopyvioQueues() | ||||
self._num_workers = num_workers | self._num_workers = num_workers | ||||
for i in range(num_workers): | for i in range(num_workers): | ||||
name = "local-{0:04}.{1}".format(id(self) % 10000, i) | |||||
name = f"local-{id(self) % 10000:04}.{i}" | |||||
_CopyvioWorker(name, self._queues, self._until).start() | _CopyvioWorker(name, self._queues, self._until).start() | ||||
def _calculate_confidence(self, delta): | def _calculate_confidence(self, delta): | ||||
"""Return the confidence of a violation as a float between 0 and 1.""" | """Return the confidence of a violation as a float between 0 and 1.""" | ||||
def conf_with_article_and_delta(article, delta): | def conf_with_article_and_delta(article, delta): | ||||
"""Calculate confidence using the article and delta chain sizes.""" | """Calculate confidence using the article and delta chain sizes.""" | ||||
# This piecewise function exhibits exponential growth until it | # This piecewise function exhibits exponential growth until it | ||||
@@ -377,7 +396,7 @@ class CopyvioWorkspace: | |||||
if ratio <= 0.52763: | if ratio <= 0.52763: | ||||
return -log(1 - ratio) | return -log(1 - ratio) | ||||
else: | else: | ||||
return (-0.8939 * (ratio ** 2)) + (1.8948 * ratio) - 0.0009 | |||||
return (-0.8939 * (ratio**2)) + (1.8948 * ratio) - 0.0009 | |||||
def conf_with_delta(delta): | def conf_with_delta(delta): | ||||
"""Calculate confidence using just the delta chain size.""" | """Calculate confidence using just the delta chain size.""" | ||||
@@ -395,8 +414,12 @@ class CopyvioWorkspace: | |||||
return (delta - 50) / delta | return (delta - 50) / delta | ||||
d_size = float(delta.size) | d_size = float(delta.size) | ||||
return abs(max(conf_with_article_and_delta(self._article.size, d_size), | |||||
conf_with_delta(d_size))) | |||||
return abs( | |||||
max( | |||||
conf_with_article_and_delta(self._article.size, d_size), | |||||
conf_with_delta(d_size), | |||||
) | |||||
) | |||||
def _finish_early(self): | def _finish_early(self): | ||||
"""Finish handling links prematurely (if we've hit min_confidence).""" | """Finish handling links prematurely (if we've hit min_confidence).""" | ||||
@@ -418,12 +441,12 @@ class CopyvioWorkspace: | |||||
self.sources.append(source) | self.sources.append(source) | ||||
if self._exclude_check and self._exclude_check(url): | if self._exclude_check and self._exclude_check(url): | ||||
self._logger.debug("enqueue(): exclude {0}".format(url)) | |||||
self._logger.debug(f"enqueue(): exclude {url}") | |||||
source.excluded = True | source.excluded = True | ||||
source.skip() | source.skip() | ||||
continue | continue | ||||
if self._short_circuit and self.finished: | if self._short_circuit and self.finished: | ||||
self._logger.debug("enqueue(): auto-skip {0}".format(url)) | |||||
self._logger.debug(f"enqueue(): auto-skip {url}") | |||||
source.skip() | source.skip() | ||||
continue | continue | ||||
@@ -431,6 +454,7 @@ class CopyvioWorkspace: | |||||
key = tldextract.extract(url).registered_domain | key = tldextract.extract(url).registered_domain | ||||
except ImportError: # Fall back on very naive method | except ImportError: # Fall back on very naive method | ||||
from urllib.parse import urlparse | from urllib.parse import urlparse | ||||
key = ".".join(urlparse(url).netloc.split(".")[-2:]) | key = ".".join(urlparse(url).netloc.split(".")[-2:]) | ||||
logmsg = "enqueue(): {0} {1} -> {2}" | logmsg = "enqueue(): {0} {1} -> {2}" | ||||
@@ -450,7 +474,7 @@ class CopyvioWorkspace: | |||||
conf = self._calculate_confidence(delta) | conf = self._calculate_confidence(delta) | ||||
else: | else: | ||||
conf = 0.0 | conf = 0.0 | ||||
self._logger.debug("compare(): {0} -> {1}".format(source.url, conf)) | |||||
self._logger.debug(f"compare(): {source.url} -> {conf}") | |||||
with self._finish_lock: | with self._finish_lock: | ||||
if source_chain: | if source_chain: | ||||
source.update(conf, source_chain, delta) | source.update(conf, source_chain, delta) | ||||
@@ -463,7 +487,7 @@ class CopyvioWorkspace: | |||||
def wait(self): | def wait(self): | ||||
"""Wait for the workers to finish handling the sources.""" | """Wait for the workers to finish handling the sources.""" | ||||
self._logger.debug("Waiting on {0} sources".format(len(self.sources))) | |||||
self._logger.debug(f"Waiting on {len(self.sources)} sources") | |||||
for source in self.sources: | for source in self.sources: | ||||
source.join(self._until) | source.join(self._until) | ||||
with self._finish_lock: | with self._finish_lock: | ||||
@@ -474,6 +498,7 @@ class CopyvioWorkspace: | |||||
def get_result(self, num_queries=0): | def get_result(self, num_queries=0): | ||||
"""Return a CopyvioCheckResult containing the results of this check.""" | """Return a CopyvioCheckResult containing the results of this check.""" | ||||
def cmpfunc(s1, s2): | def cmpfunc(s1, s2): | ||||
if s2.confidence != s1.confidence: | if s2.confidence != s1.confidence: | ||||
return 1 if s2.confidence > s1.confidence else -1 | return 1 if s2.confidence > s1.confidence else -1 | ||||
@@ -482,6 +507,11 @@ class CopyvioWorkspace: | |||||
return int(s1.skipped) - int(s2.skipped) | return int(s1.skipped) - int(s2.skipped) | ||||
self.sources.sort(cmpfunc) | self.sources.sort(cmpfunc) | ||||
return CopyvioCheckResult(self.finished, self.sources, num_queries, | |||||
time.time() - self._start_time, self._article, | |||||
self.possible_miss) | |||||
return CopyvioCheckResult( | |||||
self.finished, | |||||
self.sources, | |||||
num_queries, | |||||
time.time() - self._start_time, | |||||
self._article, | |||||
self.possible_miss, | |||||
) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2019 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,9 +18,9 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from hashlib import md5 | |||||
from logging import getLogger, NullHandler | |||||
import re | import re | ||||
from hashlib import md5 | |||||
from logging import NullHandler, getLogger | |||||
from time import gmtime, strftime | from time import gmtime, strftime | ||||
from urllib.parse import quote | from urllib.parse import quote | ||||
@@ -33,6 +31,7 @@ from earwigbot.wiki.copyvios import CopyvioMixIn | |||||
__all__ = ["Page"] | __all__ = ["Page"] | ||||
class Page(CopyvioMixIn): | class Page(CopyvioMixIn): | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Page** | **EarwigBot: Wiki Toolset: Page** | ||||
@@ -75,13 +74,13 @@ class Page(CopyvioMixIn): | |||||
- :py:meth:`~earwigbot.wiki.copyvios.CopyrightMixIn.copyvio_compare`: | - :py:meth:`~earwigbot.wiki.copyvios.CopyrightMixIn.copyvio_compare`: | ||||
checks the page like :py:meth:`copyvio_check`, but against a specific URL | checks the page like :py:meth:`copyvio_check`, but against a specific URL | ||||
""" | """ | ||||
PAGE_UNKNOWN = 0 | PAGE_UNKNOWN = 0 | ||||
PAGE_INVALID = 1 | PAGE_INVALID = 1 | ||||
PAGE_MISSING = 2 | PAGE_MISSING = 2 | ||||
PAGE_EXISTS = 3 | PAGE_EXISTS = 3 | ||||
def __init__(self, site, title, follow_redirects=False, pageid=None, | |||||
logger=None): | |||||
def __init__(self, site, title, follow_redirects=False, pageid=None, logger=None): | |||||
"""Constructor for new Page instances. | """Constructor for new Page instances. | ||||
Takes four arguments: a Site object, the Page's title (or pagename), | Takes four arguments: a Site object, the Page's title (or pagename), | ||||
@@ -145,7 +144,7 @@ class Page(CopyvioMixIn): | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the Page.""" | """Return a nice string representation of the Page.""" | ||||
return '<Page "{0}" of {1}>'.format(self.title, str(self.site)) | |||||
return f'<Page "{self.title}" of {str(self.site)}>' | |||||
def _assert_validity(self): | def _assert_validity(self): | ||||
"""Used to ensure that our page's title is valid. | """Used to ensure that our page's title is valid. | ||||
@@ -157,7 +156,7 @@ class Page(CopyvioMixIn): | |||||
contains "[") it will always be invalid, and cannot be edited. | contains "[") it will always be invalid, and cannot be edited. | ||||
""" | """ | ||||
if self._exists == self.PAGE_INVALID: | if self._exists == self.PAGE_INVALID: | ||||
e = "Page '{0}' is invalid.".format(self._title) | |||||
e = f"Page '{self._title}' is invalid." | |||||
raise exceptions.InvalidPageError(e) | raise exceptions.InvalidPageError(e) | ||||
def _assert_existence(self): | def _assert_existence(self): | ||||
@@ -169,7 +168,7 @@ class Page(CopyvioMixIn): | |||||
""" | """ | ||||
self._assert_validity() | self._assert_validity() | ||||
if self._exists == self.PAGE_MISSING: | if self._exists == self.PAGE_MISSING: | ||||
e = "Page '{0}' does not exist.".format(self._title) | |||||
e = f"Page '{self._title}' does not exist." | |||||
raise exceptions.PageNotFoundError(e) | raise exceptions.PageNotFoundError(e) | ||||
def _load(self): | def _load(self): | ||||
@@ -208,9 +207,15 @@ class Page(CopyvioMixIn): | |||||
""" | """ | ||||
if not result: | if not result: | ||||
query = self.site.api_query | query = self.site.api_query | ||||
result = query(action="query", prop="info|revisions", | |||||
inprop="protection|url", rvprop="user", rvlimit=1, | |||||
rvdir="newer", titles=self._title) | |||||
result = query( | |||||
action="query", | |||||
prop="info|revisions", | |||||
inprop="protection|url", | |||||
rvprop="user", | |||||
rvlimit=1, | |||||
rvdir="newer", | |||||
titles=self._title, | |||||
) | |||||
if "interwiki" in result["query"]: | if "interwiki" in result["query"]: | ||||
self._title = result["query"]["interwiki"][0]["title"] | self._title = result["query"]["interwiki"][0]["title"] | ||||
@@ -247,7 +252,7 @@ class Page(CopyvioMixIn): | |||||
# These last two fields will only be specified if the page exists: | # These last two fields will only be specified if the page exists: | ||||
self._lastrevid = res.get("lastrevid") | self._lastrevid = res.get("lastrevid") | ||||
try: | try: | ||||
self._creator = res['revisions'][0]['user'] | |||||
self._creator = res["revisions"][0]["user"] | |||||
except KeyError: | except KeyError: | ||||
pass | pass | ||||
@@ -263,9 +268,14 @@ class Page(CopyvioMixIn): | |||||
""" | """ | ||||
if not result: | if not result: | ||||
query = self.site.api_query | query = self.site.api_query | ||||
result = query(action="query", prop="revisions", rvlimit=1, | |||||
rvprop="content|timestamp", rvslots="main", | |||||
titles=self._title) | |||||
result = query( | |||||
action="query", | |||||
prop="revisions", | |||||
rvlimit=1, | |||||
rvprop="content|timestamp", | |||||
rvslots="main", | |||||
titles=self._title, | |||||
) | |||||
res = list(result["query"]["pages"].values())[0] | res = list(result["query"]["pages"].values())[0] | ||||
try: | try: | ||||
@@ -279,8 +289,19 @@ class Page(CopyvioMixIn): | |||||
self._load_attributes() | self._load_attributes() | ||||
self._assert_existence() | self._assert_existence() | ||||
def _edit(self, params=None, text=None, summary=None, minor=None, bot=None, | |||||
force=None, section=None, captcha_id=None, captcha_word=None, **kwargs): | |||||
def _edit( | |||||
self, | |||||
params=None, | |||||
text=None, | |||||
summary=None, | |||||
minor=None, | |||||
bot=None, | |||||
force=None, | |||||
section=None, | |||||
captcha_id=None, | |||||
captcha_word=None, | |||||
**kwargs, | |||||
): | |||||
"""Edit the page! | """Edit the page! | ||||
If *params* is given, we'll use it as our API query parameters. | If *params* is given, we'll use it as our API query parameters. | ||||
@@ -296,9 +317,18 @@ class Page(CopyvioMixIn): | |||||
# Build our API query string: | # Build our API query string: | ||||
if not params: | if not params: | ||||
params = self._build_edit_params(text, summary, minor, bot, force, | |||||
section, captcha_id, captcha_word, kwargs) | |||||
else: # Make sure we have the right token: | |||||
params = self._build_edit_params( | |||||
text, | |||||
summary, | |||||
minor, | |||||
bot, | |||||
force, | |||||
section, | |||||
captcha_id, | |||||
captcha_word, | |||||
kwargs, | |||||
) | |||||
else: # Make sure we have the right token: | |||||
params["token"] = self.site.get_token() | params["token"] = self.site.get_token() | ||||
# Try the API query, catching most errors with our handler: | # Try the API query, catching most errors with our handler: | ||||
@@ -319,14 +349,29 @@ class Page(CopyvioMixIn): | |||||
# Otherwise, there was some kind of problem. Throw an exception: | # Otherwise, there was some kind of problem. Throw an exception: | ||||
raise exceptions.EditError(result["edit"]) | raise exceptions.EditError(result["edit"]) | ||||
def _build_edit_params(self, text, summary, minor, bot, force, section, | |||||
captcha_id, captcha_word, kwargs): | |||||
def _build_edit_params( | |||||
self, | |||||
text, | |||||
summary, | |||||
minor, | |||||
bot, | |||||
force, | |||||
section, | |||||
captcha_id, | |||||
captcha_word, | |||||
kwargs, | |||||
): | |||||
"""Given some keyword arguments, build an API edit query string.""" | """Given some keyword arguments, build an API edit query string.""" | ||||
unitxt = text.encode("utf8") if isinstance(text, str) else text | unitxt = text.encode("utf8") if isinstance(text, str) else text | ||||
hashed = md5(unitxt).hexdigest() # Checksum to ensure text is correct | hashed = md5(unitxt).hexdigest() # Checksum to ensure text is correct | ||||
params = { | params = { | ||||
"action": "edit", "title": self._title, "text": text, | |||||
"token": self.site.get_token(), "summary": summary, "md5": hashed} | |||||
"action": "edit", | |||||
"title": self._title, | |||||
"text": text, | |||||
"token": self.site.get_token(), | |||||
"summary": summary, | |||||
"md5": hashed, | |||||
} | |||||
if section: | if section: | ||||
params["section"] = section | params["section"] = section | ||||
@@ -365,9 +410,16 @@ class Page(CopyvioMixIn): | |||||
is protected), or we'll try to fix it (for example, if the token is | is protected), or we'll try to fix it (for example, if the token is | ||||
invalid, we'll try to get a new one). | invalid, we'll try to get a new one). | ||||
""" | """ | ||||
perms = ["noedit", "noedit-anon", "cantcreate", "cantcreate-anon", | |||||
"protectedtitle", "noimageredirect", "noimageredirect-anon", | |||||
"blocked"] | |||||
perms = [ | |||||
"noedit", | |||||
"noedit-anon", | |||||
"cantcreate", | |||||
"cantcreate-anon", | |||||
"protectedtitle", | |||||
"noimageredirect", | |||||
"noimageredirect-anon", | |||||
"blocked", | |||||
] | |||||
if error.code in perms: | if error.code in perms: | ||||
raise exceptions.PermissionsError(error.info) | raise exceptions.PermissionsError(error.info) | ||||
elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | ||||
@@ -551,7 +603,7 @@ class Page(CopyvioMixIn): | |||||
""" | """ | ||||
if self._namespace < 0: | if self._namespace < 0: | ||||
ns = self.site.namespace_id_to_name(self._namespace) | ns = self.site.namespace_id_to_name(self._namespace) | ||||
e = "Pages in the {0} namespace can't have talk pages.".format(ns) | |||||
e = f"Pages in the {ns} namespace can't have talk pages." | |||||
raise exceptions.InvalidPageError(e) | raise exceptions.InvalidPageError(e) | ||||
if self._is_talkpage: | if self._is_talkpage: | ||||
@@ -587,9 +639,15 @@ class Page(CopyvioMixIn): | |||||
# Kill two birds with one stone by doing an API query for both our | # Kill two birds with one stone by doing an API query for both our | ||||
# attributes and our page content: | # attributes and our page content: | ||||
query = self.site.api_query | query = self.site.api_query | ||||
result = query(action="query", rvlimit=1, titles=self._title, | |||||
prop="info|revisions", inprop="protection|url", | |||||
rvprop="content|timestamp", rvslots="main") | |||||
result = query( | |||||
action="query", | |||||
rvlimit=1, | |||||
titles=self._title, | |||||
prop="info|revisions", | |||||
inprop="protection|url", | |||||
rvprop="content|timestamp", | |||||
rvslots="main", | |||||
) | |||||
self._load_attributes(result=result) | self._load_attributes(result=result) | ||||
self._assert_existence() | self._assert_existence() | ||||
self._load_content(result=result) | self._load_content(result=result) | ||||
@@ -674,8 +732,9 @@ class Page(CopyvioMixIn): | |||||
the page was deleted/recreated between getting our edit token and | the page was deleted/recreated between getting our edit token and | ||||
editing our page. Be careful with this! | editing our page. Be careful with this! | ||||
""" | """ | ||||
self._edit(text=text, summary=summary, minor=minor, bot=bot, | |||||
force=force, **kwargs) | |||||
self._edit( | |||||
text=text, summary=summary, minor=minor, bot=bot, force=force, **kwargs | |||||
) | |||||
def add_section(self, text, title, minor=False, bot=True, force=False, **kwargs): | def add_section(self, text, title, minor=False, bot=True, force=False, **kwargs): | ||||
"""Add a new section to the bottom of the page. | """Add a new section to the bottom of the page. | ||||
@@ -687,8 +746,15 @@ class Page(CopyvioMixIn): | |||||
This should create the page if it does not already exist, with just the | This should create the page if it does not already exist, with just the | ||||
new section as content. | new section as content. | ||||
""" | """ | ||||
self._edit(text=text, summary=title, minor=minor, bot=bot, force=force, | |||||
section="new", **kwargs) | |||||
self._edit( | |||||
text=text, | |||||
summary=title, | |||||
minor=minor, | |||||
bot=bot, | |||||
force=force, | |||||
section="new", | |||||
**kwargs, | |||||
) | |||||
def check_exclusion(self, username=None, optouts=None): | def check_exclusion(self, username=None, optouts=None): | ||||
"""Check whether or not we are allowed to edit the page. | """Check whether or not we are allowed to edit the page. | ||||
@@ -710,6 +776,7 @@ class Page(CopyvioMixIn): | |||||
``{{bots|optout=all}}``, but `True` on | ``{{bots|optout=all}}``, but `True` on | ||||
``{{bots|optout=orfud,norationale,replaceable}}``. | ``{{bots|optout=orfud,norationale,replaceable}}``. | ||||
""" | """ | ||||
def parse_param(template, param): | def parse_param(template, param): | ||||
value = template.get(param).value | value = template.get(param).value | ||||
return [item.strip().lower() for item in value.split(",")] | return [item.strip().lower() for item in value.split(",")] | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -22,7 +20,7 @@ | |||||
from http.cookiejar import CookieJar | from http.cookiejar import CookieJar | ||||
from json import dumps | from json import dumps | ||||
from logging import getLogger, NullHandler | |||||
from logging import NullHandler, getLogger | |||||
from os.path import expanduser | from os.path import expanduser | ||||
from threading import RLock | from threading import RLock | ||||
from time import sleep, time | from time import sleep, time | ||||
@@ -228,11 +226,11 @@ class Site: | |||||
) | ) | ||||
) | ) | ||||
name, password = self._login_info | name, password = self._login_info | ||||
login = "({0}, {1})".format(repr(name), "hidden" if password else None) | |||||
login = "({}, {})".format(repr(name), "hidden" if password else None) | |||||
oauth = "hidden" if self._oauth else None | oauth = "hidden" if self._oauth else None | ||||
cookies = self._cookiejar.__class__.__name__ | cookies = self._cookiejar.__class__.__name__ | ||||
if hasattr(self._cookiejar, "filename"): | if hasattr(self._cookiejar, "filename"): | ||||
cookies += "({0!r})".format(getattr(self._cookiejar, "filename")) | |||||
cookies += "({!r})".format(getattr(self._cookiejar, "filename")) | |||||
else: | else: | ||||
cookies += "()" | cookies += "()" | ||||
agent = self.user_agent | agent = self.user_agent | ||||
@@ -267,26 +265,26 @@ class Site: | |||||
since_last_query = time() - self._last_query_time # Throttling support | since_last_query = time() - self._last_query_time # Throttling support | ||||
if since_last_query < self._wait_between_queries: | if since_last_query < self._wait_between_queries: | ||||
wait_time = self._wait_between_queries - since_last_query | wait_time = self._wait_between_queries - since_last_query | ||||
log = "Throttled: waiting {0} seconds".format(round(wait_time, 2)) | |||||
log = f"Throttled: waiting {round(wait_time, 2)} seconds" | |||||
self._logger.debug(log) | self._logger.debug(log) | ||||
sleep(wait_time) | sleep(wait_time) | ||||
self._last_query_time = time() | self._last_query_time = time() | ||||
url, params = self._build_api_query(params, ignore_maxlag, no_assert) | url, params = self._build_api_query(params, ignore_maxlag, no_assert) | ||||
if "lgpassword" in params: | if "lgpassword" in params: | ||||
self._logger.debug("{0} -> <hidden>".format(url)) | |||||
self._logger.debug(f"{url} -> <hidden>") | |||||
else: | else: | ||||
data = dumps(params) | data = dumps(params) | ||||
if len(data) > 1000: | if len(data) > 1000: | ||||
self._logger.debug("{0} -> {1}...".format(url, data[:997])) | |||||
self._logger.debug(f"{url} -> {data[:997]}...") | |||||
else: | else: | ||||
self._logger.debug("{0} -> {1}".format(url, data)) | |||||
self._logger.debug(f"{url} -> {data}") | |||||
try: | try: | ||||
response = self._session.post(url, data=params) | response = self._session.post(url, data=params) | ||||
response.raise_for_status() | response.raise_for_status() | ||||
except requests.RequestException as exc: | except requests.RequestException as exc: | ||||
raise exceptions.APIError("API query failed: {0}".format(exc)) | |||||
raise exceptions.APIError(f"API query failed: {exc}") | |||||
return self._handle_api_result(response, params, tries, wait, ae_retry) | return self._handle_api_result(response, params, tries, wait, ae_retry) | ||||
@@ -602,7 +600,7 @@ class Site: | |||||
elif res == "WrongPass" or res == "WrongPluginPass": | elif res == "WrongPass" or res == "WrongPluginPass": | ||||
e = "The given password is incorrect." | e = "The given password is incorrect." | ||||
else: | else: | ||||
e = "Couldn't login; server says '{0}'.".format(res) | |||||
e = f"Couldn't login; server says '{res}'." | |||||
raise exceptions.LoginError(e) | raise exceptions.LoginError(e) | ||||
def _logout(self): | def _logout(self): | ||||
@@ -915,7 +913,7 @@ class Site: | |||||
else: | else: | ||||
return self._namespaces[ns_id][0] | return self._namespaces[ns_id][0] | ||||
except KeyError: | except KeyError: | ||||
e = "There is no namespace with id {0}.".format(ns_id) | |||||
e = f"There is no namespace with id {ns_id}." | |||||
raise exceptions.NamespaceNotFoundError(e) | raise exceptions.NamespaceNotFoundError(e) | ||||
def namespace_name_to_id(self, name): | def namespace_name_to_id(self, name): | ||||
@@ -933,7 +931,7 @@ class Site: | |||||
if lname in lnames: | if lname in lnames: | ||||
return ns_id | return ns_id | ||||
e = "There is no namespace with name '{0}'.".format(name) | |||||
e = f"There is no namespace with name '{name}'." | |||||
raise exceptions.NamespaceNotFoundError(e) | raise exceptions.NamespaceNotFoundError(e) | ||||
def get_page(self, title, follow_redirects=False, pageid=None): | def get_page(self, title, follow_redirects=False, pageid=None): | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,13 +18,13 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from collections import OrderedDict | |||||
import errno | import errno | ||||
from http.cookiejar import LWPCookieJar, LoadError | |||||
import sqlite3 as sqlite | |||||
import stat | |||||
from collections import OrderedDict | |||||
from http.cookiejar import LoadError, LWPCookieJar | |||||
from os import chmod, path | from os import chmod, path | ||||
from platform import python_version | from platform import python_version | ||||
import stat | |||||
import sqlite3 as sqlite | |||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
from earwigbot.exceptions import SiteNotFoundError | from earwigbot.exceptions import SiteNotFoundError | ||||
@@ -78,7 +76,7 @@ class SitesDB: | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the SitesDB.""" | """Return a nice string representation of the SitesDB.""" | ||||
return "<SitesDB at {0}>".format(self._sitesdb) | |||||
return f"<SitesDB at {self._sitesdb}>" | |||||
def _get_cookiejar(self): | def _get_cookiejar(self): | ||||
"""Return a LWPCookieJar object loaded from our .cookies file. | """Return a LWPCookieJar object loaded from our .cookies file. | ||||
@@ -102,7 +100,7 @@ class SitesDB: | |||||
self._cookiejar.load() | self._cookiejar.load() | ||||
except LoadError: | except LoadError: | ||||
pass # File contains bad data, so ignore it completely | pass # File contains bad data, so ignore it completely | ||||
except IOError as e: | |||||
except OSError as e: | |||||
if e.errno == errno.ENOENT: # "No such file or directory" | if e.errno == errno.ENOENT: # "No such file or directory" | ||||
# Create the file and restrict reading/writing only to the | # Create the file and restrict reading/writing only to the | ||||
# owner, so others can't peak at our cookies: | # owner, so others can't peak at our cookies: | ||||
@@ -149,7 +147,7 @@ class SitesDB: | |||||
query1 = "SELECT * FROM sites WHERE site_name = ?" | query1 = "SELECT * FROM sites WHERE site_name = ?" | ||||
query2 = "SELECT sql_data_key, sql_data_value FROM sql_data WHERE sql_site = ?" | query2 = "SELECT sql_data_key, sql_data_value FROM sql_data WHERE sql_site = ?" | ||||
query3 = "SELECT ns_id, ns_name, ns_is_primary_name FROM namespaces WHERE ns_site = ?" | query3 = "SELECT ns_id, ns_name, ns_is_primary_name FROM namespaces WHERE ns_site = ?" | ||||
error = "Site '{0}' not found in the sitesdb.".format(name) | |||||
error = f"Site '{name}' not found in the sitesdb." | |||||
with sqlite.connect(self._sitesdb) as conn: | with sqlite.connect(self._sitesdb) as conn: | ||||
try: | try: | ||||
site_data = conn.execute(query1, (name,)).fetchone() | site_data = conn.execute(query1, (name,)).fetchone() | ||||
@@ -263,7 +261,7 @@ class SitesDB: | |||||
if site: | if site: | ||||
return site[0] | return site[0] | ||||
else: | else: | ||||
url = "//{0}.{1}.%".format(lang, project) | |||||
url = f"//{lang}.{project}.%" | |||||
site = conn.execute(query2, (url,)).fetchone() | site = conn.execute(query2, (url,)).fetchone() | ||||
return site[0] if site else None | return site[0] if site else None | ||||
except sqlite.OperationalError: | except sqlite.OperationalError: | ||||
@@ -322,7 +320,7 @@ class SitesDB: | |||||
else: | else: | ||||
conn.execute("DELETE FROM sql_data WHERE sql_site = ?", (name,)) | conn.execute("DELETE FROM sql_data WHERE sql_site = ?", (name,)) | ||||
conn.execute("DELETE FROM namespaces WHERE ns_site = ?", (name,)) | conn.execute("DELETE FROM namespaces WHERE ns_site = ?", (name,)) | ||||
self._logger.info("Removed site '{0}'".format(name)) | |||||
self._logger.info(f"Removed site '{name}'") | |||||
return True | return True | ||||
def get_site(self, name=None, project=None, lang=None): | def get_site(self, name=None, project=None, lang=None): | ||||
@@ -379,7 +377,7 @@ class SitesDB: | |||||
name = self._get_site_name_from_sitesdb(project, lang) | name = self._get_site_name_from_sitesdb(project, lang) | ||||
if name: | if name: | ||||
return self._get_site_object(name) | return self._get_site_object(name) | ||||
e = "Site '{0}:{1}' not found in the sitesdb.".format(project, lang) | |||||
e = f"Site '{project}:{lang}' not found in the sitesdb." | |||||
raise SiteNotFoundError(e) | raise SiteNotFoundError(e) | ||||
def add_site( | def add_site( | ||||
@@ -412,7 +410,7 @@ class SitesDB: | |||||
if not project or not lang: | if not project or not lang: | ||||
e = "Without a base_url, both a project and a lang must be given." | e = "Without a base_url, both a project and a lang must be given." | ||||
raise SiteNotFoundError(e) | raise SiteNotFoundError(e) | ||||
base_url = "//{0}.{1}.org".format(lang, project) | |||||
base_url = f"//{lang}.{project}.org" | |||||
cookiejar = self._get_cookiejar() | cookiejar = self._get_cookiejar() | ||||
config = self.config | config = self.config | ||||
@@ -443,7 +441,7 @@ class SitesDB: | |||||
wait_between_queries=wait_between_queries, | wait_between_queries=wait_between_queries, | ||||
) | ) | ||||
self._logger.info("Added site '{0}'".format(site.name)) | |||||
self._logger.info(f"Added site '{site.name}'") | |||||
self._add_site_to_sitesdb(site) | self._add_site_to_sitesdb(site) | ||||
return self._get_site_object(site.name) | return self._get_site_object(site.name) | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -20,9 +18,9 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from logging import getLogger, NullHandler | |||||
from logging import NullHandler, getLogger | |||||
from socket import AF_INET, AF_INET6, inet_pton | |||||
from time import gmtime, strptime | from time import gmtime, strptime | ||||
from socket import AF_INET, AF_INET6, error as socket_error, inet_pton | |||||
from earwigbot.exceptions import UserNotFoundError | from earwigbot.exceptions import UserNotFoundError | ||||
from earwigbot.wiki import constants | from earwigbot.wiki import constants | ||||
@@ -30,6 +28,7 @@ from earwigbot.wiki.page import Page | |||||
__all__ = ["User"] | __all__ = ["User"] | ||||
class User: | class User: | ||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: User** | **EarwigBot: Wiki Toolset: User** | ||||
@@ -88,11 +87,11 @@ class User: | |||||
def __repr__(self): | def __repr__(self): | ||||
"""Return the canonical string representation of the User.""" | """Return the canonical string representation of the User.""" | ||||
return "User(name={0!r}, site={1!r})".format(self._name, self._site) | |||||
return f"User(name={self._name!r}, site={self._site!r})" | |||||
def __str__(self): | def __str__(self): | ||||
"""Return a nice string representation of the User.""" | """Return a nice string representation of the User.""" | ||||
return '<User "{0}" of {1}>'.format(self.name, str(self.site)) | |||||
return f'<User "{self.name}" of {str(self.site)}>' | |||||
def _get_attribute(self, attr): | def _get_attribute(self, attr): | ||||
"""Internally used to get an attribute by name. | """Internally used to get an attribute by name. | ||||
@@ -106,7 +105,7 @@ class User: | |||||
if not hasattr(self, attr): | if not hasattr(self, attr): | ||||
self._load_attributes() | self._load_attributes() | ||||
if not self._exists: | if not self._exists: | ||||
e = "User '{0}' does not exist.".format(self._name) | |||||
e = f"User '{self._name}' does not exist." | |||||
raise UserNotFoundError(e) | raise UserNotFoundError(e) | ||||
return getattr(self, attr) | return getattr(self, attr) | ||||
@@ -117,8 +116,9 @@ class User: | |||||
is not defined. This defines it. | is not defined. This defines it. | ||||
""" | """ | ||||
props = "blockinfo|groups|rights|editcount|registration|emailable|gender" | props = "blockinfo|groups|rights|editcount|registration|emailable|gender" | ||||
result = self.site.api_query(action="query", list="users", | |||||
ususers=self._name, usprop=props) | |||||
result = self.site.api_query( | |||||
action="query", list="users", ususers=self._name, usprop=props | |||||
) | |||||
res = result["query"]["users"][0] | res = result["query"]["users"][0] | ||||
# normalize our username in case it was entered oddly | # normalize our username in case it was entered oddly | ||||
@@ -136,7 +136,7 @@ class User: | |||||
self._blockinfo = { | self._blockinfo = { | ||||
"by": res["blockedby"], | "by": res["blockedby"], | ||||
"reason": res["blockreason"], | "reason": res["blockreason"], | ||||
"expiry": res["blockexpiry"] | |||||
"expiry": res["blockexpiry"], | |||||
} | } | ||||
except KeyError: | except KeyError: | ||||
self._blockinfo = False | self._blockinfo = False | ||||
@@ -280,10 +280,10 @@ class User: | |||||
""" | """ | ||||
try: | try: | ||||
inet_pton(AF_INET, self.name) | inet_pton(AF_INET, self.name) | ||||
except socket_error: | |||||
except OSError: | |||||
try: | try: | ||||
inet_pton(AF_INET6, self.name) | inet_pton(AF_INET6, self.name) | ||||
except socket_error: | |||||
except OSError: | |||||
return False | return False | ||||
return True | return True | ||||
@@ -302,7 +302,7 @@ class User: | |||||
conventions are followed. | conventions are followed. | ||||
""" | """ | ||||
prefix = self.site.namespace_id_to_name(constants.NS_USER) | prefix = self.site.namespace_id_to_name(constants.NS_USER) | ||||
pagename = ':'.join((prefix, self._name)) | |||||
pagename = ":".join((prefix, self._name)) | |||||
return Page(self.site, pagename) | return Page(self.site, pagename) | ||||
def get_talkpage(self): | def get_talkpage(self): | ||||
@@ -312,5 +312,5 @@ class User: | |||||
conventions are followed. | conventions are followed. | ||||
""" | """ | ||||
prefix = self.site.namespace_id_to_name(constants.NS_USER_TALK) | prefix = self.site.namespace_id_to_name(constants.NS_USER_TALK) | ||||
pagename = ':'.join((prefix, self._name)) | |||||
pagename = ":".join((prefix, self._name)) | |||||
return Page(self.site, pagename) | return Page(self.site, pagename) |
@@ -0,0 +1,6 @@ | |||||
[tool.ruff] | |||||
target-version = "py311" | |||||
[tool.ruff.lint] | |||||
select = ["E4", "E7", "E9", "F", "I", "UP"] | |||||
ignore = ["F403"] |
@@ -1,6 +1,4 @@ | |||||
#! /usr/bin/env python | #! /usr/bin/env python | ||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -21,7 +19,7 @@ | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
from setuptools import setup, find_packages | |||||
from setuptools import find_packages, setup | |||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
@@ -69,7 +67,7 @@ setup( | |||||
url="https://github.com/earwig/earwigbot", | url="https://github.com/earwig/earwigbot", | ||||
description="EarwigBot is a Python robot that edits Wikipedia and interacts with people over IRC.", | description="EarwigBot is a Python robot that edits Wikipedia and interacts with people over IRC.", | ||||
long_description=long_docs, | long_description=long_docs, | ||||
download_url="https://github.com/earwig/earwigbot/tarball/v{0}".format(__version__), | |||||
download_url=f"https://github.com/earwig/earwigbot/tarball/v{__version__}", | |||||
keywords="earwig earwigbot irc wikipedia wiki mediawiki", | keywords="earwig earwigbot irc wikipedia wiki mediawiki", | ||||
license="MIT License", | license="MIT License", | ||||
classifiers=[ | classifiers=[ | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -39,18 +37,19 @@ Fake objects: | |||||
""" | """ | ||||
import logging | import logging | ||||
from os import path | |||||
import re | import re | ||||
from os import path | |||||
from threading import Lock | from threading import Lock | ||||
from unittest import TestCase | from unittest import TestCase | ||||
from earwigbot.bot import Bot | from earwigbot.bot import Bot | ||||
from earwigbot.commands import CommandManager | from earwigbot.commands import CommandManager | ||||
from earwigbot.config import BotConfig | from earwigbot.config import BotConfig | ||||
from earwigbot.irc import IRCConnection, Data | |||||
from earwigbot.irc import Data, IRCConnection | |||||
from earwigbot.tasks import TaskManager | from earwigbot.tasks import TaskManager | ||||
from earwigbot.wiki import SitesDB | from earwigbot.wiki import SitesDB | ||||
class CommandTestCase(TestCase): | class CommandTestCase(TestCase): | ||||
re_sender = re.compile(":(.*?)!(.*?)@(.*?)\Z") | re_sender = re.compile(":(.*?)!(.*?)@(.*?)\Z") | ||||
@@ -75,17 +74,17 @@ class CommandTestCase(TestCase): | |||||
self.assertIn(line, msgs) | self.assertIn(line, msgs) | ||||
def assertSaid(self, msg): | def assertSaid(self, msg): | ||||
self.assertSent("PRIVMSG #channel :{0}".format(msg)) | |||||
self.assertSent(f"PRIVMSG #channel :{msg}") | |||||
def assertSaidIn(self, msgs): | def assertSaidIn(self, msgs): | ||||
msgs = ["PRIVMSG #channel :{0}".format(msg) for msg in msgs] | |||||
msgs = [f"PRIVMSG #channel :{msg}" for msg in msgs] | |||||
self.assertSentIn(msgs) | self.assertSentIn(msgs) | ||||
def assertReply(self, msg): | def assertReply(self, msg): | ||||
self.assertSaid("\x02Foo\x0F: {0}".format(msg)) | |||||
self.assertSaid(f"\x02Foo\x0f: {msg}") | |||||
def assertReplyIn(self, msgs): | def assertReplyIn(self, msgs): | ||||
msgs = ["\x02Foo\x0F: {0}".format(msg) for msg in msgs] | |||||
msgs = [f"\x02Foo\x0f: {msg}" for msg in msgs] | |||||
self.assertSaidIn(msgs) | self.assertSaidIn(msgs) | ||||
def maker(self, line, chan, msg=None): | def maker(self, line, chan, msg=None): | ||||
@@ -98,7 +97,7 @@ class CommandTestCase(TestCase): | |||||
return data | return data | ||||
def make_msg(self, command, *args): | def make_msg(self, command, *args): | ||||
line = ":Foo!bar@example.com PRIVMSG #channel :!{0}".format(command) | |||||
line = f":Foo!bar@example.com PRIVMSG #channel :!{command}" | |||||
line = line.strip().split() | line = line.strip().split() | ||||
line.extend(args) | line.extend(args) | ||||
return self.maker(line, line[2], " ".join(line[3:])[1:]) | return self.maker(line, line[2], " ".join(line[3:])[1:]) | ||||
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,8 +23,8 @@ import unittest | |||||
from earwigbot.commands.calc import Command | from earwigbot.commands.calc import Command | ||||
from tests import CommandTestCase | from tests import CommandTestCase | ||||
class TestCalc(CommandTestCase): | |||||
class TestCalc(CommandTestCase): | |||||
def setUp(self): | def setUp(self): | ||||
super().setUp(Command) | super().setUp(Command) | ||||
@@ -55,5 +53,6 @@ class TestCalc(CommandTestCase): | |||||
self.command.process(self.make_msg("calc", *q)) | self.command.process(self.make_msg("calc", *q)) | ||||
self.assertReply(test[1]) | self.assertReply(test[1]) | ||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
unittest.main(verbosity=2) | unittest.main(verbosity=2) |
@@ -1,5 +1,3 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | ||||
# | # | ||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
@@ -25,8 +23,8 @@ import unittest | |||||
from earwigbot.commands.test import Command | from earwigbot.commands.test import Command | ||||
from tests import CommandTestCase | from tests import CommandTestCase | ||||
class TestTest(CommandTestCase): | |||||
class TestTest(CommandTestCase): | |||||
def setUp(self): | def setUp(self): | ||||
super().setUp(Command) | super().setUp(Command) | ||||
@@ -40,10 +38,11 @@ class TestTest(CommandTestCase): | |||||
def test_process(self): | def test_process(self): | ||||
def test(): | def test(): | ||||
self.command.process(self.make_msg("test")) | self.command.process(self.make_msg("test")) | ||||
self.assertSaidIn(["Hey \x02Foo\x0F!", "'sup \x02Foo\x0F?"]) | |||||
self.assertSaidIn(["Hey \x02Foo\x0f!", "'sup \x02Foo\x0f?"]) | |||||
for i in range(64): | for i in range(64): | ||||
test() | test() | ||||
if __name__ == "__main__": | if __name__ == "__main__": | ||||
unittest.main(verbosity=2) | unittest.main(verbosity=2) |