diff --git a/report.py b/report.py new file mode 100644 index 0000000..d58c25e --- /dev/null +++ b/report.py @@ -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 diff --git a/retootbot.py b/retootbot.py index be15756..78744e6 100755 --- a/retootbot.py +++ b/retootbot.py @@ -9,14 +9,14 @@ import time import trigger import logging import sendmail +import report logger = logging.getLogger(__name__) class RetootBot(object): - def __init__(self, config, trigger): + def __init__(self, config): self.config = config - self.trigger = trigger self.client_id = self.register() self.m = self.login() @@ -53,34 +53,61 @@ class RetootBot(object): ) return m - def retoot(self, toots=()): + def crawl(self): + """ + Crawl mentions from Mastodon. + + :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 + 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: + pickle.dump(self.seen_toots, f) + 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 toot in toots: - self.m.toot(toot) + for report in reports: + self.post(report) # 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: - - # save state - 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) - os.rename('seen_toots.pickle.part', 'seen_toots.pickle') + for mention in self.crawl(): + if not trigger.is_ok(mention.text): + continue + self.repost(mention) + retoots.append(mention) # return mentions for mirroring return retoots @@ -96,11 +123,11 @@ if __name__ == '__main__': logger.addHandler(fh) trigger = trigger.Trigger(config) - bot = RetootBot(config, trigger) + bot = RetootBot(config) try: while True: - bot.retoot() + bot.flow(trigger) time.sleep(1) except KeyboardInterrupt: print("Good bye. Remember to restart the bot!") diff --git a/retweetbot.py b/retweetbot.py index 84f3b56..4a48da9 100755 --- a/retweetbot.py +++ b/retweetbot.py @@ -19,18 +19,14 @@ class RetweetBot(object): api: The api object, generated with your oAuth keys, responsible for 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 """ - 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. - :param trigger: object of the trigger :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 Tweet """ @@ -46,7 +42,6 @@ class RetweetBot(object): self.history_path = history_path self.last_mention = self.get_history(self.history_path) - self.trigger = trigger self.waitcounter = 0 def get_api_keys(self): @@ -101,19 +96,7 @@ class RetweetBot(object): self.waitcounter -= 1 return self.waitcounter - def format_mastodon(self, status): - """ - 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): + def crawl(self): """ crawls all Tweets which mention the bot from the twitter rest API. @@ -134,20 +117,20 @@ class RetweetBot(object): self.waitcounter += 10 return [] - def retweet(self, status): + def repost(self, status): """ Retweets a given tweet. - :param status: A tweet object. + :param status: (report.Report object) :return: toot: string of the tweet, to toot on mastodon. """ while 1: try: self.api.retweet(status.id) - logger.info("Retweeted: " + self.format_mastodon(status)) + logger.info("Retweeted: " + status.format()) if status.id > self.last_mention: self.last_mention = status.id - return self.format_mastodon(status) + return status.format() except requests.exceptions.ConnectionError: logger.error("Twitter API Error: Bad Connection", exc_info=True) sleep(10) @@ -158,67 +141,76 @@ class RetweetBot(object): self.last_mention = status.id return None - def tweet(self, post): + def post(self, status): """ Tweet a post. - :param post: String with the text to tweet. + :param status: (report.Report object) """ - if len(post) > 280: - post = post[:280 - 4] + u' ...' + text = status.format() + if len(text) > 280: + text = status.text[:280 - 4] + u' ...' while 1: try: - self.api.update_status(status=post) + self.api.update_status(status=text) return except requests.exceptions.ConnectionError: logger.error("Twitter API Error: Bad Connection", exc_info=True) sleep(10) - def flow(self, to_tweet=()): + def flow(self, trigger, to_tweet=()): """ The flow of crawling mentions and retweeting them. :param to_tweet: list of strings to tweet :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: - self.tweet(post) + self.post(post) # Store all mentions in a list of Status Objects - mentions = self.crawl_mentions() - mastodon = [] + mentions = self.crawl() + + # initialise list of strings for other bots + all_tweets = [] for status in mentions: # Is the Text of the Tweet in the triggerlist? - if self.trigger.is_ok(status.text): + if trigger.is_ok(status.text): # Retweet status - toot = self.retweet(status) + toot = self.repost(status) if toot: - mastodon.append(toot) + all_tweets.append(toot) # save the id so it doesn't get crawled again if status.id > self.last_mention: self.last_mention = status.id self.save_last_mention() - # Return Retweets for tooting on mastodon - return mastodon + # Return Retweets for posting on other bots + return all_tweets if __name__ == "__main__": - # create an Api object + # get the config dict of dicts with open('config.toml') as configfile: config = toml.load(configfile) + # set log file fh = logging.FileHandler(config['logging']['logpath']) fh.setLevel(logging.DEBUG) logger.addHandler(fh) + # initialise trigger trigger = trigger.Trigger(config) + + # initialise twitter bot bot = RetweetBot(trigger, config) + try: while True: - bot.flow() + # :todo separate into small functions + bot.flow(trigger) sleep(60) except KeyboardInterrupt: print("Good bye. Remember to restart the bot!") diff --git a/ticketfrei.py b/ticketfrei.py index a796689..ad554ab 100755 --- a/ticketfrei.py +++ b/ticketfrei.py @@ -9,7 +9,6 @@ from retootbot import RetootBot from retweetbot import RetweetBot from trigger import Trigger - if __name__ == '__main__': # read config in TOML format (https://github.com/toml-lang/toml#toml) with open('config.toml') as configfile: @@ -21,14 +20,14 @@ if __name__ == '__main__': logger.addHandler(fh) trigger = Trigger(config) - mbot = RetootBot(config, trigger) - tbot = RetweetBot(trigger, config) + mbot = RetootBot(config) + tbot = RetweetBot(config) try: statuses = [] while True: - statuses = mbot.retoot(statuses) - statuses = tbot.flow(statuses) + statuses = mbot.flow(trigger, statuses) + statuses = tbot.flow(trigger, to_tweet=statuses) time.sleep(60) except KeyboardInterrupt: print("Good bye. Remember to restart the bot!")