diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..202d74f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,596 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + no-value-for-parameter + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/janine/__init__.py b/janine/__init__.py index e69de29..e4f9adc 100644 --- a/janine/__init__.py +++ b/janine/__init__.py @@ -0,0 +1,16 @@ +''' +This file is part of JANINE. + +JANINE is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +JANINE is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with JANINE. If not, see . +''' diff --git a/janine/janine.py b/janine/janine.py index 840486e..62ff9ec 100644 --- a/janine/janine.py +++ b/janine/janine.py @@ -1,11 +1,25 @@ +''' +This file is part of JANINE. + +JANINE is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +JANINE is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with JANINE. If not, see . +''' + import os import sys import json -import datetime import configparser -import threading import logging -import signal import asyncio from collections import namedtuple @@ -14,23 +28,23 @@ from aioxmpp.structs import PresenceShow import requests -from janine.utils import find_one, find_all +from janine.utils import find_one, make_msg, format_warning from janine.sources import sources_from_config, MiscDataSources log = logging.getLogger('janine') log.setLevel(logging.INFO) log.addHandler(logging.StreamHandler()) -Warning = namedtuple('Warning', ['id', - 'sent', - 'effective_from', - 'expires', - 'urgency', - 'sender', - 'headline', - 'description', - 'instruction', - 'landkreise']) +Warning_ = namedtuple('Warning_', ['id', + 'sent', + 'effective_from', + 'expires', + 'urgency', + 'sender', + 'headline', + 'description', + 'instruction', + 'landkreise']) def stub_warning(id_): @@ -38,35 +52,35 @@ def stub_warning(id_): Returns a stubbed warning for loading warnings from disk. The only real attribute is the @id_ . ''' - return Warning(id=id_, - sent='', - effective_from='', - expires='', - urgency='', - sender='', - headline='', - description='', - instruction='', - landkreise=[]) + return Warning_(id=id_, + sent='', + effective_from='', + expires='', + urgency='', + sender='', + headline='', + description='', + instruction='', + landkreise=[]) def to_warning(data): ''' Returns a Warning given the raw data ''' info = find_one(lambda x: 'headline' in x.keys(), data['info']) - return Warning(id=data['identifier'], - sent=data['sent'], - # Not all items have to have those - effective_from=info.get('effective', 'N/A'), - # Not all items have to have those - expires=info.get('expires', 'N/A'), - urgency=info['urgency'], - # Not all items have to have those - sender=info.get('senderName', 'N/A'), - headline=info['headline'], - description=info['description'], - instruction=info.get('instruction', 'N/A'), - landkreise=get_landkreise(data)) + return Warning_(id=data['identifier'], + sent=data['sent'], + # Not all items have to have those + effective_from=info.get('effective', 'N/A'), + # Not all items have to have those + expires=info.get('expires', 'N/A'), + urgency=info['urgency'], + # Not all items have to have those + sender=info.get('senderName', 'N/A'), + headline=info['headline'], + description=info['description'], + instruction=info.get('instruction', 'N/A'), + landkreise=get_landkreise(data)) def get_landkreise(data): ''' @@ -76,7 +90,7 @@ def get_landkreise(data): geocode = find_one(lambda e: 'geocode' in e.keys(), info['area']) # Note: Some items may have multiple Landkreise - return list(map(lambda e: e['valueName'], geocode['geocode'])) + return list(map(lambda e: e['valueName'], geocode['geocode'])) def parse_data(text): ''' @@ -86,6 +100,10 @@ def parse_data(text): return [to_warning(el) for el in data] class WarningBot: + ''' + This class represents the actual bot. The only things + to be done is call connect() after creating an instance. + ''' def __init__(self): self._warnings0 = [] self._warnings1 = [] @@ -105,28 +123,28 @@ class WarningBot: Starts the "event loop" of the bot ''' self._client = aioxmpp.PresenceManagedClient( - self._jid, - aioxmpp.make_security_layer(self._password)) + self._jid, + aioxmpp.make_security_layer(self._password)) - async with self._client.connected() as stream: + async with self._client.connected(): log.info('Client connected to server') # In case you want a nice avatar if self._avatar: log.info('Setting avatar') - with open(self._avatar, 'rb') as f: - image_data = f.read() + with open(self._avatar, 'rb') as avatar_file: + image_data = avatar_file.read() avatar_set = aioxmpp.avatar.AvatarSet() avatar_set.add_avatar_image('image/png', image_bytes=image_data) await (self._client.summon(aioxmpp.avatar.AvatarService) - .publish_avatar_set(avatar_set)) + .publish_avatar_set(avatar_set)) # Set some presence information self._client.set_presence( - aioxmpp.PresenceState(available=True, - show=PresenceShow.CHAT), - self._status) + aioxmpp.PresenceState(available=True, + show=PresenceShow.CHAT), + self._status) # Enable Carbons await self._client.summon(aioxmpp.CarbonsClient).enable() @@ -134,9 +152,9 @@ class WarningBot: # Register the message handler self._client.stream.register_message_callback( - aioxmpp.MessageType.CHAT, - None, - self._handle_message) + aioxmpp.MessageType.CHAT, + None, + self._handle_message) log.info('Message handler registered') # Start our fetch-send loop @@ -145,7 +163,7 @@ class WarningBot: # use async in event handlers loop = asyncio.get_event_loop() periodic = loop.create_task(self._periodic_requests()) - log.info('Periodic ticker started') + log.info('Periodic ticker started') await periodic def __is_message_valid(self, msg): @@ -154,25 +172,14 @@ class WarningBot: ''' if msg.type_ != aioxmpp.MessageType.CHAT: return False - + if not msg.body: return False - + if msg.from_.domain != self._jid.domain and self._same_domain: return False return True - - def __make_msg(self, to, body): - ''' - Wrapper for creating a message object to enqueue or send. - ''' - msg = aioxmpp.Message( - type_=aioxmpp.MessageType.CHAT, - to=to) - msg.body[None] = body - - return msg def _handle_message(self, msg): # Handle cases we don't want to deal with @@ -185,74 +192,78 @@ class WarningBot: if cmd == 'subscribe': # Do we have a landkreis? if len(cmd_parts) < 2: - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, - body='Du hast keinen Landkreis angegeben')) + body='Du hast keinen Landkreis angegeben')) return # Check if the entered Landkreis is valid landkreis = ' '.join(cmd_parts[1:]) if not landkreis in self._channels: - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, - body='Der angegebene Landkreis ist ungültig')) + body='Der angegebene Landkreis ist ungültig')) return - - if not landkreis in self._warn_clients.keys(): + + if landkreis not in self._warn_clients.keys(): self._warn_clients[landkreis] = [] self._warn_clients[landkreis].append(str(msg.from_.bare())) - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, body=f'Du erhälst nun Nachrichten zu {landkreis} von mir')) - with open(self._client_store, 'w') as cf: - cf.write(json.dumps(self._warn_clients)) + with open(self._client_store, 'w') as clients_file: + clients_file.write(json.dumps(self._warn_clients)) # Send all known warnings for the landkreis to the user for warning in self._warnings0: if landkreis in warning.landkreise: - body = self._format_warning(warning) - self._client.enqueue(self.__make_msg( + body = format_warning(warning) + self._client.enqueue(make_msg( to=msg.from_, body=body)) elif cmd == 'unsubscribe': # Do we have a landkreis? if len(cmd_parts) < 2: - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, - body='Du hast keinen Landkreis angegeben')) + body='Du hast keinen Landkreis angegeben')) return - + landkreis = ' '.join(cmd_parts[1:]) - if not landkreis in self._warn_clients: - self._client.enqueue(self.__make_msg( + if landkreis not in self._warn_clients: + self._client.enqueue(make_msg( to=msg.from_, - body=f'Du hast {landkreis} nicht abonniert')) + body=f'Du hast {landkreis} nicht abonniert')) return if str(msg.from_.bare()) in self._warn_clients[landkreis]: filter_ = lambda x: x != str(msg.from_.bare()) self._warn_clients[landkreis] = list(filter(filter_, self._warn_clients[landkreis])) - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, - body=f'Du erhälst keine Nachrichten zu {landkreis} mehr von mir')) - + body=f'Du erhälst keine Nachrichten zu {landkreis} mehr von mir')) + if len(self._warn_clients[landkreis]) == 0: del self._warn_clients[landkreis] else: - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, body=f'Du hast {landkreis} nicht abonniert')) elif cmd == 'help': - body = 'Verfügbare Befehle:\n\nsubscribe - Abonniere einen Landkreis\nunsubscribe - Entferne das Abonnement zu einem Landkreis\nhelp - Gebe diese Hilfe aus' - self._client.enqueue(self.__make_msg( + body = '''Verfügbare Befehle: + +subscribe - Abonniere einen Landkreis +unsubscribe - Entferne das Abonnement zu einem Landkreis +help - Gebe diese Hilfe aus''' + self._client.enqueue(make_msg( to=msg.from_, body=body)) else: - self._client.enqueue(self.__make_msg( + self._client.enqueue(make_msg( to=msg.from_, body='Diesen Befehl kenne ich nicht... Mit "help" kannst du alle Befehle sehen, die ich kenne.')) @@ -265,11 +276,11 @@ class WarningBot: while True: log.debug('Refreshing warning list') await self._fetch_warnings() - + # Flush the warnings to disk ids = list(map(lambda x: x.id, self._warnings0)) - with open(self._warnings_file, 'w') as wf: - wf.write(json.dumps(ids)) + with open(self._warnings_file, 'w') as warnings_file: + warnings_file.write(json.dumps(ids)) await asyncio.sleep(self._refresh_timeout) @@ -283,15 +294,15 @@ class WarningBot: for source in self._sources: req = requests.get(source) self._warnings0 = parse_data(req.text) - + # Find new warnings and send the new ones ids = map(lambda x: x.id, self._warnings1) for warning in self._warnings0: if warning.id in ids: continue - + if len(set(warning.landkreise).intersection(self._warn_clients.keys())): - body = self._format_warning(warning) + body = format_warning(warning) for landkreis in warning.landkreise: for to in self._warn_clients.get(landkreis, []): msg = aioxmpp.stanza.Message( @@ -300,87 +311,54 @@ class WarningBot: msg.body[None] = body await self._client.send(msg) - def __time_format(self, time_str): - ''' - Reformat ISO style time data to a more - readable format. - ''' - date = None - try: - date = datetime.datetime.fromisoformat(time_str) - except: - pass - - if not date: - return time_str - - return '{}.{}.{} {}:{}'.format(date.day, - date.month, - date.year, - date.hour, - date.minute) - - def _format_warning(self, warning): - ''' - Send a warning to all the recipients - ''' - # Reformat the message a bit - effective_time = self.__time_format(warning.effective_from) - expiry_time = self.__time_format(warning.expires) - body = f'*{warning.headline}*\n({effective_time} bis {expiry_time})\n\n{warning.description}' - - # Smells like script injection, but okay - body = body.replace('
', '\n') - body = body.replace('
', '\n') - return body - def _load_config(self): # Load config config_path = sys.argv[1] if len(sys.argv) == 2 else '/etc/janine/janine.conf' config = configparser.ConfigParser() config.read(config_path) - + # Configure sources self._sources = sources_from_config(config) self._data_dir = config['General'].get('DataDir', '/etc/janine') self._recipients = config['General']['Recipients'].split(',') self._refresh_timeout = int(config['General']['Timeout']) self._same_domain = config['General'].get('SameDomain', 'True') == 'True' - + # Persistent data # Subscribed clients self._client_store = os.path.join(self._data_dir, 'clients.json') if os.path.exists(self._client_store): - with open(self._client_store, 'r') as cf: - self._warn_clients = json.loads(cf.read()) + with open(self._client_store, 'r') as clients_file: + self._warn_clients = json.loads(clients_file.read()) log.info('Clients read from disk') - + self._warnings_file = os.path.join(self._data_dir, 'warnings.json') if os.path.exists(self._warnings_file): - with open(self._warnings_file, 'r') as wf: - self._warnings0 = list(map(stub_warning, json.loads(wf.read()))) + with open(self._warnings_file, 'r') as warnings_file: + self._warnings0 = list(map(stub_warning, + json.loads(warnings_file.read()))) log.info('Warnings read from disk') # Landkreise self._channels = [] channels = {} - channel_file = os.path.join(self._data_dir, 'channels.json') - if not os.path.exists(channel_file): + self._channel_file = os.path.join(self._data_dir, 'channels.json') + if not os.path.exists(self._channel_file): log.info('Requesting search channels') req = requests.get(MiscDataSources.channels()) channels = json.loads(req.text) self._channels = list(map(lambda key: channels[key].get('NAME', ''), - channels.keys())) + channels.keys())) try: - with open(channel_file, 'w') as f: - f.write(json.dumps(self._channels)) + with open(self._channel_file, 'w') as channel_file: + channel_file.write(json.dumps(self._channels)) except Exception as err: log.error('Failed to cache channel data:') log.error(str(err)) else: - with open(channel_file, 'r') as f: - self._channels = json.loads(f.read()) + with open(self._channel_file, 'r') as channel_file: + self._channels = json.loads(channel_file.read()) log.info('Search channels read from disk') # Bot Config @@ -390,6 +368,9 @@ class WarningBot: self._status = config['Bot'].get('Status', 'Warnt dich vor Katastrophen') def main(): + ''' + Main function. + ''' bot = WarningBot() loop = asyncio.get_event_loop() loop.run_until_complete(bot.connect()) diff --git a/janine/sources.py b/janine/sources.py index d31ad01..8a50f0d 100644 --- a/janine/sources.py +++ b/janine/sources.py @@ -1,3 +1,20 @@ +''' +This file is part of JANINE. + +JANINE is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +JANINE is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with JANINE. If not, see . +''' + class WarningSources: ''' A collection of sources of the BBK diff --git a/janine/utils.py b/janine/utils.py index 978c063..aa3ff56 100644 --- a/janine/utils.py +++ b/janine/utils.py @@ -1,3 +1,64 @@ +''' +This file is part of JANINE. + +JANINE is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +JANINE is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with JANINE. If not, see . +''' + +import datetime + +import aioxmpp + +def make_msg(self, to, body): + ''' + Wrapper for creating a message object to enqueue or send. + ''' + msg = aioxmpp.Message( + type_=aioxmpp.MessageType.CHAT, + to=to) + msg.body[None] = body + + return msg + +def format_time(self, time_str): + ''' + Reformat ISO style time data to a more + readable format. + ''' + try: + date = datetime.datetime.fromisoformat(time_str) + except ValueError: + return time_str + + return f'{date.day}.{date.month}.{date.year} {date.hour}:{date.minute}' + +def format_warning(self, warning): + ''' + Send a warning to all the recipients + ''' + # Reformat the message a bit + effective_time = format_time(warning.effective_from) + expiry_time = format_time(warning.expires) + body = f'''*{warning.headline}* +({effective_time} bis {expiry_time}) + +{warning.description}''' + + # Smells like script injection, but okay + body = body.replace('
', '\n') + body = body.replace('
', '\n') + return body + def find_one(func, array): ''' Utility function