forked from ticketfrei/ticketfrei
Standardized reports; moved flow() logic to crawl(), repost(), & post(); bots don't own Trigger anymore
This commit is contained in:
parent
9f060b405e
commit
ff73c5dc21
36
report.py
Normal file
36
report.py
Normal 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
|
83
retootbot.py
83
retootbot.py
|
@ -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):
|
||||||
|
"""
|
||||||
|
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
|
# toot external provided messages
|
||||||
for toot in toots:
|
for report in reports:
|
||||||
self.m.toot(toot)
|
self.post(report)
|
||||||
|
|
||||||
# boost mentions
|
# boost mentions
|
||||||
retoots = []
|
retoots = []
|
||||||
for notification in self.m.notifications():
|
for mention in self.crawl():
|
||||||
if (notification['type'] == 'mention'
|
if not trigger.is_ok(mention.text):
|
||||||
and notification['status']['id'] not in self.seen_toots):
|
continue
|
||||||
self.seen_toots.add(notification['status']['id'])
|
self.repost(mention)
|
||||||
text_content = re.sub(r'<[^>]*>', '',
|
retoots.append(mention)
|
||||||
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')
|
|
||||||
|
|
||||||
# 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!")
|
||||||
|
|
|
@ -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!")
|
||||||
|
|
|
@ -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!")
|
||||||
|
|
Loading…
Reference in a new issue