123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935 |
- #!/usr/bin/env python3
- import argparse
- import configparser
- import daemon
- import getpass
- import json
- import logging
- import os, os.path
- import re
- import signal
- import sys
- import time
- import bs4
- import crontab
- import mastodon
- import requests
- import custom
- from pprint import pprint
- # =============================================================================
- # CONSTANTS
- # =============================================================================
- BOT_VERSION_NUMBER = "dev"
- BOT_VERSION = "Boostbot - v" + BOT_VERSION_NUMBER
- BOT_APPNAME = "Boostbot"
- BOT_DATA_DEFAULT = { "client_id" : None,
- "client_secret" : None,
- "login" : None,
- "access_token" : None,
- "username" : None,
- "instance" : None,
- "instance_url" : None,
- "master_username" : None,
- "master_instance" : None,
- "users_blacklist" : [],
- "sources" : { "timeline" : {},
- "hashtag" : {},
- "mentions" : None } }
- # =============================================================================
- # ARGUMENTS
- # =============================================================================
- parser = argparse.ArgumentParser()
- parser.add_argument ( "-V", "--version",
- action = "store_true",
- help = "show bot version" )
- parser.add_argument ( "-D", "--debug",
- action = "store_true",
- help = "activate debug mode" )
- parser.add_argument ( "-s", "--simulate",
- action = "store_true",
- help = "do not actually post anything or save anything" )
- parser.add_argument ( "-S", "--nopost",
- action = "store_true",
- help = "do not actually post anything (but still save data)" )
- parser.add_argument ( "-F", "--foreground",
- action = "store_true",
- help = "run in foreground" )
- parser.add_argument ( "-v", "--verbose",
- action = "count",
- default = 0,
- help = "print more verbose logs; repeat to increase verbosity" )
- parser.add_argument ( "-f", "--config-file",
- default = "/etc/mastodon/boostbot.conf",
- help = "indicates the main config file to use" )
- parser.add_argument ( "-d", "--data-file",
- default = "/var/run/mastodon/boostbot.data",
- help = "indicates the data file to use" )
- args = parser.parse_args()
- # =============================================================================
- # LOGGING
- # =============================================================================
- log = logging.getLogger(BOT_APPNAME)
- log.setLevel(logging.DEBUG)
- log_handler_console = logging.StreamHandler()
- if args.verbose > 1:
- log_handler_console.setLevel(logging.DEBUG)
- elif args.verbose > 0:
- log_handler_console.setLevel(logging.INFO)
- else:
- log_handler_console.setLevel(logging.WARNING)
- try:
- import pycolog
- log_formatter_console = pycolog.ColoredFormatter("%(messages)s")
- except ImportError:
- log_formatter_console = logging.Formatter("%(name)s:%(levelname)s: %(message)s")
- log_handler_console.setFormatter(log_formatter_console)
- log.addHandler(log_handler_console)
- # =============================================================================
- #FUNCTIONS
- # =============================================================================
- # -----------------------------------------------------------------------------
- def load_data(datafile):
- logger = logging.getLogger(BOT_APPNAME)
- data = {}
- try:
- logger.debug("Loading data from {}".format(datafile))
- with open(datafile, 'r') as file:
- data = json.load(file)
- except FileNotFoundError:
- logger.warning("No data file {} found".format(datafile))
- # Set default values if they do not exist
- for d in BOT_DATA_DEFAULT:
- if not d in data:
- logger.warning("'{}' not in data, adding default value".format(d))
- data[d] = BOT_DATA_DEFAULT[d]
- return data
- # -----------------------------------------------------------------------------
- def save_data(data, datafilename):
- logger = logging.getLogger(BOT_APPNAME)
- logger.debug("Saving data to '{}'".format(datafilename))
- with open(datafilename,'w') as file:
- json.dump(data,file)
- # -----------------------------------------------------------------------------
- def mark_toot_as_treated(mastodon, toot, interacted_with=False):
- if interacted_with:
- mastodon.status_favourite(toot["id"])
- # -----------------------------------------------------------------------------
- def is_toot_treated(mastodon, toot):
- return toot["favourited"]
- # =============================================================================
- # CLASSES
- # =============================================================================
- # -----------------------------------------------------------------------------
- class Rule:
- # -------------------------------------------------------------------------
- def __init__(self, name, config_section):
- self.name = name
- self.action = config_section.get("action", fallback=None)
- self.action_info = config_section.get("action_info", fallback=None)
- self.debug = config_section.getboolean("debug", fallback=False)
- # -------------------------------------------------------------------------
- def __str__(self):
- return self.name
- # -------------------------------------------------------------------------
- def __repr__(self):
- return self.name
- # -------------------------------------------------------------------------
- def act(self, mastodon, context, data, toot=None, really_act=True):
- logger = logging.getLogger(BOT_APPNAME)
- logger.info("Acting according to rule '{}':" \
- .format(self.name) )
- # FOLLOW ACTION
- if self.action == "follow":
- logger.info( "Following @{}" \
- .format( toot["account"]["acct"] ) )
- if not really_act:
- return
- mastodon.account_follow(toot["account"]["id"])
- return
- # UNFOLLOW ACTION
- if self.action == "unfollow":
- logger.info( "Unfollowing @{}"\
- .format( toot["account"]["acct"] ) )
- if not really_act:
- return
- mastodon.account_unfollow(toot["account"]["id"])
- return
- # BOOST ACTION
- if self.action == "boost":
- logger.info( "Boosting toot n°{} by @{}" \
- .format( toot["id"],
- toot["account"]["acct"] ) )
- if not really_act:
- return
- mastodon.status_reblog(toot["id"])
- return
- # For the next actions, add toot author to context
- try:
- context["author"] = toot["account"]["acct"]
- except:
- pass
- # ANSWER ACTION
- if self.action == "answer":
- status = "@{} " \
- .format(context["author"]) \
- + self.action_info \
- .format(**context)
- logger.info( "Answering toot n°{} by @{}" \
- .format( toot["id"],
- toot["account"]["acct"] ) )
- if not really_act:
- return
- mastodon.status_post( status = status,
- in_reply_to_id = toot["id"] )
- return
- # TOOT ACTION
- if self.action == "toot":
- status = self.action_info \
- .format(**context)
- logger.info( "Tooting '{}'" \
- .format( status ) )
- if not really_act:
- return
- mastodon.status_post( status = status )
- return
- # BLACKLIST ACTION
- if self.action == "blacklist":
- author = toot["account"]["acct"]
- logger.info( "Blacklisting @{}" \
- .format( author ) )
- if not really_act:
- return
- status = "@{} ".format(author)
- if not author in data["users_blacklist"]:
- data["users_blacklist"].append(author)
- status += "Blacklisted."
- else:
- status += "Already blacklisted."
- mastodon.status_post( status = status,
- in_reply_to_id = toot["id"] )
- return
- # WHITELIST ACTION
- if self.action == "whitelist":
- author = toot["account"]["acct"]
- logger.info( "Whitelisting @{}" \
- .format( author ) )
- if not really_act:
- return
- status = "@{} ".format(author)
- if not author in data["users_blacklist"]:
- status += "Already not blacklisted."
- else:
- data["users_blacklist"].remove(author)
- status += "Unblacklisted."
- mastodon.status_post( status = status )
- return
- # CUSTOM ACTION
- if self.action == "custom":
- logger.info( "Running custom action '{}'" \
- .format( self.action_info ) )
- if not really_act:
- return
- function = getattr(custom, self.action_info)
- return function(mastodon, context, toot)
- # -----------------------------------------------------------------------------
- class ReactRule(Rule):
- """A Rule for a reaction of the bot: an action is triggered if the rule
- matches a toot from a monitored source."""
- # -------------------------------------------------------------------------
- def __init__(self, name, config_section):
- super(ReactRule, self).__init__(name, config_section)
- #
- self.is_answered = config_section. \
- getboolean( "answered",
- fallback=False )
- #
- self.is_addressed = config_section. \
- getboolean( "addressed",
- fallback=False )
- #
- self.is_mentionned = config_section. \
- getboolean( "mentionned",
- fallback=self.is_addressed )
- # Compile this rule pattern
- pattern = config_section.get("pattern", fallback="")
- ignore_case = config_section.getboolean("ignore_case", fallback=True)
- flags = 0
- if ignore_case:
- flags = flags | re.IGNORECASE
- #
- self.pattern = re.compile( pattern, flags )
- # -------------------------------------------------------------------------
- def matches(self, mastodon, context, data, toot, really_act=True):
- logger = logging.getLogger(BOT_APPNAME)
- if self.debug:
- logger.debug("Matching reaction rule {}".format(self.name))
- # Check that we are not talking to ourselves, which would make for some
- # funny screenshots, but a useless bot.
- if toot["account"]["id"] == context["bot"]["id"]:
- if self.debug:
- logger.debug("Stop before talking to ourselves")
- return False
- # Check the reply if the rule must matched addressed
- if self.is_answered:
- if toot["in_reply_to_account_id"] != context["bot"]["id"]:
- if self.debug:
- logger.debug("This toot is not in answer to us")
- return False
- # Check the mentions if the rule must match mentionned
- if self.is_mentionned:
- accounts_mentions = map( lambda m: m["acct"], toot["mentions"] )
- if not context["bot"]["acct"] in accounts_mentions:
- if self.debug:
- logger.debug("We are not mentionned in this toot")
- return False
- # Filter out if the author is blacklisted and we are not mentionned
- if ( not self.is_mentionned
- and toot["account"]["acct"] in data["users_blacklist"] ):
- if self.debug:
- logger.debug("Author is blacklisted")
- return False
- # Parse the text
- text = bs4.BeautifulSoup( toot["content"], "html.parser").get_text()
- # Check if it's addressed to us, meaning we are the first mention
- if self.is_addressed:
- if not text.startswith("@{}".format(context["bot"]["username"])):
- if self.debug:
- logger.debug("This toot is not adressed to us")
- return False
- # Check if the rule matches the pattern. We use bs4 to parse html and
- # match only content
- match = self.pattern.search( text )
- if match is None :
- if self.debug:
- logger.debug("No match in text".format(text))
- return False
- toot["content_text"] = text
- self.act( mastodon, context, data, toot, really_act )
- return True
- # -----------------------------------------------------------------------------
- class ActRule(Rule):
- """A Rule for an autonomous action of the bot: the action is triggered at a
- specific time."""
- def __init__(self, name, config_section):
- super(ActRule, self).__init__(name, config_section)
- #
- self.date = crontab.CronTab( config_section.get("at") )
- # -------------------------------------------------------------------------
- def triggers( self, mastodon, context, data, sleep=30, really_act=True):
- if (self.date.next(default_utc=True) - sleep) > 0:
- return False
- self.act( mastodon, context, data, really_act=really_act )
- return True
- # -----------------------------------------------------------------------------
- class Source:
- TYPE_HASHTAG = "hashtag"
- TYPE_TIMELINE = "timeline"
- TYPE_MENTIONS = "mentions"
- # -------------------------------------------------------------------------
- def __init__(self, name, type):
- if type == self.TYPE_HASHTAG:
- if name[0] == "#":
- self.name = name[1:]
- else:
- self.name = name
- elif type == self.TYPE_TIMELINE:
- if name in ["local", "instance"]:
- self.name = "local"
- elif name in ["public", "federated", "federation", "world"]:
- self.name = "public"
- elif name in ["home", "mine", "personal"]:
- self.name = "home"
- else:
- raise ValueError("Unknown timeline name: {}".format(name))
- elif type == self.TYPE_MENTIONS:
- name = self.TYPE_MENTIONS
- else:
- raise ValueError("Unknown source type: {}".format(type))
- self.last_id = 0
- # -------------------------------------------------------------------------
- def __str__(self):
- return self.name
- # -------------------------------------------------------------------------
- def __repr__(self):
- return self.name
- # -----------------------------------------------------------------------------
- class TootQueue:
- def __init__(self):
- self._queue = []
- def __len__(self):
- return len(self._queue)
- def __iter__(self):
- return self._queue.__iter__()
- def __str__(self):
- return str(self._queue)
- def __repr__(self):
- return str(self._queue)
- def insert(self, toots):
- if(issubclass(type(toots), list)):
- rv = False
- for t in toots:
- rv2 = self.insert(t)
- rv = rv or rv2
- return rv
- toot = toots
- for idx, t in enumerate(self._queue):
- if t["id"] == toot["id"]:
- return False
- # We could do better with bisect, but this will suffice for now
- if t["created_at"] >= toot["created_at"]: # comparing datetime
- self._queue.insert(idx, toot)
- return True
- self._queue.append(toot)
- return True
- def pop(self):
- return self._queue.pop()
- # =============================================================================
- # DAEMON
- # =============================================================================
- # -----------------------------------------------------------------------------
- bot_data = {}
- bot_sources = { Source.TYPE_MENTIONS : Source( "mentions",
- Source.TYPE_MENTIONS ) }
- bot_rules = []
- bot_mstdn = None
- bot_ctx = {}
- bot_config = None
- def daemon_initialize():
- logger = logging.getLogger( BOT_APPNAME )
- global bot_data
- global bot_sources
- global bot_rules
- global bot_mstdn
- global bot_ctx
- global bot_config
- # Load data
- # =========================================================================
- bot_data = load_data( args.data_file )
- #
- # =========================================================================
- if ( ( not "client_id" in bot_data
- or bot_data["client_id"] is None )
- or ( not "client_secret" in bot_data
- or bot_data["client_secret"] is None )
- or ( not "access_token" in bot_data
- or bot_data["access_token"] is None )
- or ( not "instance_url" in bot_data
- or bot_data["instance_url"] is None ) ):
- if not args.foreground:
- logger.error( "Missing login infos in saved in bot data" )
- logger.info( "Try to run in foreground to register" )
- exit(1)
- # -----------------------------------------------------------------
- if ( not "instance_url" in bot_data
- or bot_data["instance_url"] is None ):
- handle = input("Bot full handle : @")
- try:
- handle_splitted = handle.split("@")
- bot_data["username"] = handle_splitted[0]
- bot_data["instance"] = handle_splitted[1]
- bot_data["instance_url"] = "https://" + handle_splitted[1]
- if not args.simulate:
- save_data(bot_data, args.data_file)
- except Exception as e:
- logger.error( "Could not parse full handle '@{}'" \
- .format(handle) )
- exit(1)
- logger.info( "Bot is '@{}' on instance '{}'" \
- .format(bot_data["username"], bot_data["instance_url"]) )
- # -----------------------------------------------------------------
- if ( not "master_username" in bot_data
- or bot_data["master_username"] is None ):
- handle = input("Bot's master full handle : @")
- try:
- handle_splitted = handle.split("@")
- bot_data["master_username"] = handle_splitted[0]
- bot_data["master_instance"] = handle_splitted[1]
- if not args.simulate:
- save_data(bot_data, args.data_file)
- except Exception as e:
- logger.error( "Could not parse full handle '@{}'" \
- .format(handle) )
- exit(1)
- logger.info( "Bot's master is '@{}@{}'" \
- .format(bot_data["master_username"],
- bot_data["master_instance"]) )
- # -----------------------------------------------------------------
- if ( ( not "client_id" in bot_data
- or bot_data["client_id"] is None )
- or ( not "client_secret" in bot_data
- or bot_data["client_secret"] is None ) ):
- logger.warning( "App needs to be registered" )
- client_id, client_secret = \
- mastodon.Mastodon.create_app( BOT_APPNAME,
- api_base_url = bot_data["instance_url"] )
- bot_data["client_id"] = client_id
- bot_data["client_secret"] = client_secret
- if not args.simulate:
- save_data(bot_data, args.data_file)
- # -----------------------------------------------------------------
- mstdn = mastodon.Mastodon( client_id = bot_data["client_id"],
- client_secret = bot_data["client_secret"],
- api_base_url = bot_data["instance_url"] )
- # -----------------------------------------------------------------
- if ( not "access_token" in bot_data
- or bot_data["access_token"] is None ):
- login = ""
- password = ""
- if ( not "login" in bot_data
- or bot_data["login"] is None ):
- login = input("Bot login (email): ")
- password = getpass.getpass(prompt = "Password for {}: " \
- .format(login))
- else:
- login = bot_data["login"]
- password = getpass.getpass(prompt = "Password for {}: " \
- .format(login))
- bot_data["login"] = login
- if not args.simulate:
- save_data(bot_data, args.data_file)
- logger.warning("Getting access token for {}".format(bot_data["login"]))
- try:
- access_token = mstdn.log_in( username = bot_data["login"],
- password = password )
- except Exception as e:
- logger.error("Could not get access token: {}".format(str(e)))
- exit(1)
- bot_data["access_token"] = access_token
- if not args.simulate:
- save_data(bot_data, args.data_file)
- #
- # =========================================================================
- bot_mstdn = mastodon.Mastodon( client_id = bot_data["client_id"],
- client_secret = bot_data["client_secret"],
- access_token = bot_data["access_token"],
- api_base_url = bot_data["instance_url"] )
- # Get some context infos
- # =====================================================================
- try:
- bot = bot_mstdn.account_search("{}@{}" \
- .format(bot_data["username"],
- bot_data["instance"]))[0]
- except:
- bot = None
- try:
- master = bot_mstdn.account_search("{}@{}" \
- .format(bot_data["master_username"],
- bot_data["master_instance"]))[0]
- except:
- master = None
- bot_ctx = { "bot" : bot,
- "master": master }
- # -----------------------------------------------------------------------------
- def daemon_load_config():
- logger = logging.getLogger( BOT_APPNAME )
- global bot_data
- global bot_sources
- global bot_rules
- global bot_mstdn
- global bot_ctx
- global bot_config
- # Load config
- # =========================================================================
- if not os.path.isfile( args.config_file ):
- logger.error( "Failed to parse main config file '{}'" \
- .format(args.config_file) )
- exit(1)
- bot_config = configparser.ConfigParser( allow_no_value = True )
- bot_config.read( args.config_file )
- logger.info( "Parsing main config file '{}'".format(args.config_file) )
- # Test sections
- for section in [ "sources", "rules" ]:
- if not section in bot_config:
- logger.error( "Missing section '{}' in main config file '{}'" \
- .format(section,
- args.config_file) )
- exit(1)
- # Create sources
- # =========================================================================
- # Example conf extract:
- # ---------------------------------
- # [sources]
- # timeline = home, public
- # hashtag = inktober, inktober2017
- # ---------------------------------
- for source_type in bot_config["sources"]:
- for source_name in bot_config["sources"][source_type].split(","):
- source_name = source_name.strip()
- logger.info( "Adding {} source: '{}'".format(source_type,
- source_name) )
- source = Source( source_name, source_type )
- # Set the max id saved
- if source_name in bot_data["sources"][source_type]:
- source.last_id = bot_data["sources"] \
- [source_type] \
- [source_name]
- try:
- bot_sources[source_type].append(source)
- except KeyError:
- bot_sources[source_type] = [ source ]
- # Read config to create rules
- # =========================================================================
- for rules_filename in bot_config["rules"]:
- logger.info("Parsing rule file '{}'".format(rules_filename))
- config_rules = configparser.ConfigParser()
- config_rules.read(rules_filename)
- for section in config_rules.sections():
- if "at" in config_rules[section] :
- logger.info("Adding action rule '{}'".format(section))
- bot_rules.append( ActRule( section, config_rules[section] ) )
- else:
- logger.info("Adding reaction rule '{}'".format(section))
- bot_rules.append( ReactRule( section, config_rules[section] ) )
- # -----------------------------------------------------------------------------
- def daemon_main_loop():
- logger = logging.getLogger( BOT_APPNAME )
- global bot_data
- global bot_sources
- global bot_rules
- global bot_mstdn
- global bot_ctx
- global bot_config
- logger.info("Entering main loop")
- toots_queue = TootQueue()
- while True:
- # Collect sources, since last save
- # =====================================================================
- # ---------------------------------------------------------------------
- for timeline in bot_sources[Source.TYPE_TIMELINE]:
- logger.debug("Collecting toots from timeline {}".format(timeline))
- try:
- toots = bot_mstdn.timeline( str(timeline),
- since_id = timeline.last_id )
- toots_queue.insert(toots)
- try:
- timeline.last_id = toots[0]["id"]
- except:
- pass
- logger.debug("{} toots added".format(len(toots)))
- except IOError as e:
- logger.error("Mastodon IO Error: {}".format(e))
- # ---------------------------------------------------------------------
- for hashtag in bot_sources[Source.TYPE_HASHTAG]:
- logger.debug("Collecting toots from hashtag {}".format(hashtag))
- try:
- toots = bot_mstdn.timeline_hashtag( str(hashtag),
- since_id = hashtag.last_id )
- toots_queue.insert(toots)
- try:
- hashtag.last_id = toots[0]["id"]
- except:
- pass
- logger.debug("{} toots added".format(len(toots)))
- except IOError as e:
- logger.error("Mastodon IO Error: {}".format(e))
- # Collect notifications
- # ---------------------------------------------------------------------
- mentions = bot_sources[Source.TYPE_MENTIONS]
- logger.debug("Collecting mentions")
- try:
- notifs = bot_mstdn.notifications( since_id = mentions.last_id )
- for n in notifs:
- if n["type"] == "mention":
- logger.debug("Adding mention n°{} by @{}"\
- .format(n["status"]["id"],
- n["status"]["account"]["acct"]))
- toots_queue.insert(n["status"])
- try:
- mentions.last_id = notifs[0]["id"]
- except:
- pass
- logger.debug("{} notifications checked".format(len(notifs)))
- except IOError as e:
- logger.error("Mastodon IO Error: {}".format(e))
- # Treat toots
- # =====================================================================
- while True:
- try:
- t = toots_queue.pop()
- except:
- break
- logger.debug("treating toot n°{}".format(t["id"]))
- if is_toot_treated( bot_mstdn, t ):
- continue
- # Check for patterns we want to interact with. If the pattern
- # matches, we run the actions it corresponds to
- interacted_with = False
- for rule in bot_rules:
- try:
- if rule.matches( bot_mstdn,
- context = bot_ctx,
- data = bot_data,
- toot = t,
- really_act = not args.simulate and not args.nopost ):
- logger.debug("rule '{}' matched!".format(rule))
- interacted_with = True
- break
- except AttributeError as e: # No matches() function
- if rule.debug:
- raise e
- # Else, skipping action rules...
- # Mark toot as treated. We specify if a rule really matched the
- # toot
- if not args.simulate and not args.nopost:
- mark_toot_as_treated( bot_mstdn, t, interacted_with )
- # Save timelines last_ids
- # =====================================================================
- # ---------------------------------------------------------------------
- for timeline in bot_sources[Source.TYPE_TIMELINE]:
- bot_data["sources"][Source.TYPE_TIMELINE][str(timeline)] = \
- timeline.last_id
- if not args.simulate:
- save_data(bot_data, args.data_file)
- logger.debug("Bumping timeline {} to {}".format(str(timeline),
- timeline.last_id))
- # ---------------------------------------------------------------------
- for hashtag in bot_sources[Source.TYPE_HASHTAG]:
- bot_data["sources"][Source.TYPE_HASHTAG][str(hashtag)] = \
- hashtag.last_id
- if not args.simulate:
- save_data(bot_data, args.data_file)
- logger.debug("Bumping hashtag {} to {}".format(str(hashtag),
- hashtag.last_id))
- # ---------------------------------------------------------------------
- mentions = bot_sources[Source.TYPE_MENTIONS]
- bot_data["sources"][Source.TYPE_MENTIONS] = \
- mentions.last_id
- if not args.simulate:
- save_data(bot_data, args.data_file)
- logger.debug("Bumping mentions to {}".format(mentions.last_id))
- # Write spontaneous toots
- # =====================================================================
- sleep_time = bot_config["general"].getint("sleep", fallback=30)
- for rule in bot_rules:
- try:
- if rule.triggers( bot_mstdn,
- context = bot_ctx,
- data = bot_data,
- sleep = sleep_time,
- really_act = not args.simulate and not args.nopost ):
- logger.debug("rule '{}' triggered!".format(rule))
- except AttributeError: # No triggers() function
- pass # Skiping reaction rules
- # Sleep a bit
- logger.debug("Sleeping {} seconds".format(sleep_time))
- time.sleep(sleep_time)
- # =============================================================================
- # SCRIPT
- # =============================================================================
- if args.foreground:
- daemon_initialize()
- daemon_load_config()
- daemon_main_loop()
- exit(0)
- # Run in background
- stdout = None
- stderr = None
- if args.debug:
- stdout = sys.stdout
- stderr = sys.stderr
- daemon_context = daemon.DaemonContext( stdout = stdout,
- stderr = stderr,
- working_directory = "." )
- daemon_context.signal_map = {
- signal.SIGTERM : "terminate",
- signal.SIGHUP : "terminate",
- signal.SIGUSR1 : daemon_load_config,
- }
- with daemon_context:
- daemon_initialize()
- daemon_load_config()
- daemon_main_loop()
|