Standardized reports; moved flow() logic to crawl(), repost(), & post(); bots don't own Trigger anymore

This commit is contained in:
b3yond 2018-01-18 11:41:08 +01:00
parent 9f060b405e
commit ff73c5dc21
4 changed files with 127 additions and 73 deletions

36
report.py Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python3
class Report(object):
"""
A ticketfrei report object.
Toots, Tweets, and E-Mails can be formed into ticketfrei reports.
"""
def __init__(self, author, source, text, id, timestamp):
"""
Constructor of a ticketfrei report
:param author: username of the author
:param source: mastodon, twitter, or email
:param text: the text of the report
:param id: id in the network
:param timestamp: time of the report
"""
self.author = author
self.type = source
self.text = text
self.timestamp = timestamp
self.id = id
def format(self):
"""
Format the report for bot.post()
:rtype: string
:return: toot: text to be tooted, e.g. "_b3yond: There are
uniformed controllers in the U2 at Opernhaus."
"""
strng = self.author + ": " + self.text
return strng

View file

@ -9,14 +9,14 @@ import time
import trigger import trigger
import logging import logging
import sendmail import sendmail
import report
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RetootBot(object): class RetootBot(object):
def __init__(self, config, trigger): def __init__(self, config):
self.config = config self.config = config
self.trigger = trigger
self.client_id = self.register() self.client_id = self.register()
self.m = self.login() self.m = self.login()
@ -53,34 +53,61 @@ class RetootBot(object):
) )
return m return m
def retoot(self, toots=()): def crawl(self):
# toot external provided messages """
for toot in toots: Crawl mentions from Mastodon.
self.m.toot(toot)
# boost mentions
retoots = []
for notification in self.m.notifications():
if (notification['type'] == 'mention'
and notification['status']['id'] not in self.seen_toots):
self.seen_toots.add(notification['status']['id'])
text_content = re.sub(r'<[^>]*>', '',
notification['status']['content'])
if not self.trigger.is_ok(text_content):
continue
logger.info('Boosting toot from %s: %s' % (
notification['status']['account']['acct'],
notification['status']['content']))
self.m.status_reblog(notification['status']['id'])
retoots.append('%s: %s' % (
notification['status']['account']['acct'],
re.sub(r'@\S*', '', text_content)))
# If the Mastodon instance returns interesting Errors, add them here:
:return: list of statuses
"""
all = self.m.notifications()
mentions = []
for status in all:
if (status['type'] == 'mention' and status['status']['id'] not in self.seen_toots):
# save state # save state
self.seen_toots.add(status['status']['id'])
with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'wb') as f: with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'wb') as f:
pickle.dump(self.seen_toots, f) pickle.dump(self.seen_toots, f)
os.rename('seen_toots.pickle.part', 'seen_toots.pickle') os.rename('seen_toots.pickle.part', 'seen_toots.pickle')
# add mention to mentions
mentions.append(report.Report(status['account']['acct'],
"mastodon",
re.sub(r'<[^>]*>', '', status['status']['content']),
status['status']['id'],
status['status']['created_at']))
return mentions
def repost(self, mention):
"""
Retoots a mention.
:param mention: (report.Report object)
"""
logger.info('Boosting toot from %s' % (
mention.format()))
self.m.status_reblog(mention.id)
def post(self, report):
"""
Toots a report from other sources.
:param report: (report.Report object)
"""
toot = report.format()
self.m.toot(toot)
def flow(self, trigger, reports=()):
# toot external provided messages
for report in reports:
self.post(report)
# boost mentions
retoots = []
for mention in self.crawl():
if not trigger.is_ok(mention.text):
continue
self.repost(mention)
retoots.append(mention)
# return mentions for mirroring # return mentions for mirroring
return retoots return retoots
@ -96,11 +123,11 @@ if __name__ == '__main__':
logger.addHandler(fh) logger.addHandler(fh)
trigger = trigger.Trigger(config) trigger = trigger.Trigger(config)
bot = RetootBot(config, trigger) bot = RetootBot(config)
try: try:
while True: while True:
bot.retoot() bot.flow(trigger)
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!") print("Good bye. Remember to restart the bot!")

View file

@ -19,18 +19,14 @@ class RetweetBot(object):
api: The api object, generated with your oAuth keys, responsible for api: The api object, generated with your oAuth keys, responsible for
communication with twitter rest API communication with twitter rest API
triggers: a list of words, one of them has to be in a tweet for it to be
retweeted
last_mention: the ID of the last tweet which mentioned you last_mention: the ID of the last tweet which mentioned you
""" """
def __init__(self, trigger, config, history_path="last_mention"): def __init__(self, config, history_path="last_mention"):
""" """
Initializes the bot and loads all the necessary data. Initializes the bot and loads all the necessary data.
:param trigger: object of the trigger
:param config: (dictionary) config.toml as a dictionary of dictionaries :param config: (dictionary) config.toml as a dictionary of dictionaries
:param logger: object of the logger
:param history_path: Path to the file with ID of the last retweeted :param history_path: Path to the file with ID of the last retweeted
Tweet Tweet
""" """
@ -46,7 +42,6 @@ class RetweetBot(object):
self.history_path = history_path self.history_path = history_path
self.last_mention = self.get_history(self.history_path) self.last_mention = self.get_history(self.history_path)
self.trigger = trigger
self.waitcounter = 0 self.waitcounter = 0
def get_api_keys(self): def get_api_keys(self):
@ -101,19 +96,7 @@ class RetweetBot(object):
self.waitcounter -= 1 self.waitcounter -= 1
return self.waitcounter return self.waitcounter
def format_mastodon(self, status): def crawl(self):
"""
Bridge your Retweets to mastodon.
:rtype: string
:param status: Object of a tweet.
:return: toot: text tooted on mastodon, e.g. "_b3yond: There are
uniformed controllers in the U2 at Opernhaus."
"""
toot = status.user.name + ": " + status.text
return toot
def crawl_mentions(self):
""" """
crawls all Tweets which mention the bot from the twitter rest API. crawls all Tweets which mention the bot from the twitter rest API.
@ -134,20 +117,20 @@ class RetweetBot(object):
self.waitcounter += 10 self.waitcounter += 10
return [] return []
def retweet(self, status): def repost(self, status):
""" """
Retweets a given tweet. Retweets a given tweet.
:param status: A tweet object. :param status: (report.Report object)
:return: toot: string of the tweet, to toot on mastodon. :return: toot: string of the tweet, to toot on mastodon.
""" """
while 1: while 1:
try: try:
self.api.retweet(status.id) self.api.retweet(status.id)
logger.info("Retweeted: " + self.format_mastodon(status)) logger.info("Retweeted: " + status.format())
if status.id > self.last_mention: if status.id > self.last_mention:
self.last_mention = status.id self.last_mention = status.id
return self.format_mastodon(status) return status.format()
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True) logger.error("Twitter API Error: Bad Connection", exc_info=True)
sleep(10) sleep(10)
@ -158,67 +141,76 @@ class RetweetBot(object):
self.last_mention = status.id self.last_mention = status.id
return None return None
def tweet(self, post): def post(self, status):
""" """
Tweet a post. Tweet a post.
:param post: String with the text to tweet. :param status: (report.Report object)
""" """
if len(post) > 280: text = status.format()
post = post[:280 - 4] + u' ...' if len(text) > 280:
text = status.text[:280 - 4] + u' ...'
while 1: while 1:
try: try:
self.api.update_status(status=post) self.api.update_status(status=text)
return return
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True) logger.error("Twitter API Error: Bad Connection", exc_info=True)
sleep(10) sleep(10)
def flow(self, to_tweet=()): def flow(self, trigger, to_tweet=()):
""" The flow of crawling mentions and retweeting them. """ The flow of crawling mentions and retweeting them.
:param to_tweet: list of strings to tweet :param to_tweet: list of strings to tweet
:return list of retweeted tweets, to toot on mastodon :return list of retweeted tweets, to toot on mastodon
""" """
# Tweet the toots the Retootbot gives to us # Tweet the reports from other sources
for post in to_tweet: for post in to_tweet:
self.tweet(post) self.post(post)
# Store all mentions in a list of Status Objects # Store all mentions in a list of Status Objects
mentions = self.crawl_mentions() mentions = self.crawl()
mastodon = []
# initialise list of strings for other bots
all_tweets = []
for status in mentions: for status in mentions:
# Is the Text of the Tweet in the triggerlist? # Is the Text of the Tweet in the triggerlist?
if self.trigger.is_ok(status.text): if trigger.is_ok(status.text):
# Retweet status # Retweet status
toot = self.retweet(status) toot = self.repost(status)
if toot: if toot:
mastodon.append(toot) all_tweets.append(toot)
# save the id so it doesn't get crawled again # save the id so it doesn't get crawled again
if status.id > self.last_mention: if status.id > self.last_mention:
self.last_mention = status.id self.last_mention = status.id
self.save_last_mention() self.save_last_mention()
# Return Retweets for tooting on mastodon # Return Retweets for posting on other bots
return mastodon return all_tweets
if __name__ == "__main__": if __name__ == "__main__":
# create an Api object # get the config dict of dicts
with open('config.toml') as configfile: with open('config.toml') as configfile:
config = toml.load(configfile) config = toml.load(configfile)
# set log file
fh = logging.FileHandler(config['logging']['logpath']) fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
logger.addHandler(fh) logger.addHandler(fh)
# initialise trigger
trigger = trigger.Trigger(config) trigger = trigger.Trigger(config)
# initialise twitter bot
bot = RetweetBot(trigger, config) bot = RetweetBot(trigger, config)
try: try:
while True: while True:
bot.flow() # :todo separate into small functions
bot.flow(trigger)
sleep(60) sleep(60)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!") print("Good bye. Remember to restart the bot!")

View file

@ -9,7 +9,6 @@ from retootbot import RetootBot
from retweetbot import RetweetBot from retweetbot import RetweetBot
from trigger import Trigger from trigger import Trigger
if __name__ == '__main__': if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml) # read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('config.toml') as configfile: with open('config.toml') as configfile:
@ -21,14 +20,14 @@ if __name__ == '__main__':
logger.addHandler(fh) logger.addHandler(fh)
trigger = Trigger(config) trigger = Trigger(config)
mbot = RetootBot(config, trigger) mbot = RetootBot(config)
tbot = RetweetBot(trigger, config) tbot = RetweetBot(config)
try: try:
statuses = [] statuses = []
while True: while True:
statuses = mbot.retoot(statuses) statuses = mbot.flow(trigger, statuses)
statuses = tbot.flow(statuses) statuses = tbot.flow(trigger, to_tweet=statuses)
time.sleep(60) time.sleep(60)
except KeyboardInterrupt: except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!") print("Good bye. Remember to restart the bot!")