import codecs
import html
import io
import os.path
import random
import textwrap
import string
import urllib
import bleach
from escpos import printer
import facebook
from mastodon import Mastodon
import PIL
import requests
import twitter
from bureau import Bureau, add_command, add_api
class TWrapper():
pass
class PublicRelations(Bureau):
"""
The Public relations department manages the flow of information between
the screenless office and the general public. It provides interfaces
for Twitter, Facebook, Mastodon and other electronic PR platforms.
"""
name = "Public Relations"
prefix = "PR"
version = 0
def __init__(self):
Bureau.__init__(self)
CK = codecs.decode("neV4HPasZrXMjNaliWWVUIaHA",
"rot13")
CS = codecs.decode("ntegmiu3rdFAyMczCWM0bgpydFHwYXV3WedhtPCec1Pu9qdfGy",
"rot13")
TW_CREDS = os.path.expanduser('~/.screenless/tw_creds')
if not os.path.exists(TW_CREDS):
twitter.oauth_dance("The Screenless Office", CK, CS,
TW_CREDS)
oauth_token, oauth_secret = twitter.read_token_file(TW_CREDS)
self.t = TWrapper()
self.auth = twitter.OAuth(oauth_token, oauth_secret, CK, CS)
self.t.t = twitter.Twitter(auth=self.auth)
try:
MASTO_CREDS = os.path.expanduser('~/.screenless/masto_creds')
masto_server = self.config["mastodon"]["server"]
if not os.path.exists(MASTO_CREDS):
Mastodon.create_app('screenless',
api_base_url=masto_server,
to_file=MASTO_CREDS)
# TODO: catch the error when our token is too old and throw it out
self.masto = Mastodon(client_id=MASTO_CREDS,
api_base_url=masto_server)
masto_user = self.config["mastodon"]["user"]
masto_pass = self.config["mastodon"]["password"]
self.masto.log_in(masto_user, masto_pass)
except KeyError:
print("no mastodon config found.")
print("you can add a 'mastodon:' section to PR.yml with:")
print(" server: my.server.name")
print(" user: myuser")
print(" password: mypassword")
print("skipping masto login for now...")
# setup DBs to map short codes to tweet ids
self.tweetdb = self.dbenv.open_db(b"tweetdb")
with self.dbenv.begin(db=self.tweetdb) as txn:
self.last_mast_notif = txn.get(b"last_mast_notif")
def get_tweet_id(self, tweet_hash):
"""
take a short code and look up the tweet/toot id
"""
with self.dbenv.begin(db=self.tweetdb) as txn:
tweetid = txn.get(tweet_hash.encode())
if tweetid:
return int(tweetid)
else:
return tweetid
def short_tweet_id(self, tweet_id):
"""
take a tweet id and return a short alphanumeric code
"""
shortcode = ''.join(random.choice(string.ascii_letters + string.digits)
for _ in range(5))
with self.dbenv.begin(db=self.tweetdb, write=True) as txn:
txn.put(shortcode.encode(), tweet_id.encode())
return shortcode
@add_command("tweetpic", "Post a Document Camera Image to Twitter")
def tweet_pic(self, reply_to_id=None, at_user=None):
"""
Takes a photograph using the document camera and posts it to Twitter.
"""
photo = self.send("PX", "photo")["photo"]
with open(photo, "rb") as imagefile:
imagedata = imagefile.read()
t_up = twitter.Twitter(domain='upload.twitter.com', auth=self.auth)
id_img1 = t_up.media.upload(media=imagedata)["media_id_string"]
if reply_to_id:
if at_user:
status = "@" + at_user
else:
status = " "
self.t.t.statuses.update(status=status,
in_reply_to_status_id=reply_to_id,
media_ids=id_img1)
else:
self.t.t.statuses.update(status="#screenless", media_ids=id_img1)
@add_command("fbpost", "Post to Facebook")
def post_fb(self):
"""
Takes a photograph using the document camera and posts it to Facebook.
"""
photo = self.send("PX", "photo")["photo"]
access_token = 'EAADixisn70ABADh2rEMZAYA8nGzd6ah8RFZA3URba263aCQ63ajLeTiZC5sgZCyIVSmRZBWReVsO9IuaLibX5RjW9Ja2tTZAbxgrDr1dPJzyGwcGTSV9bW1W4NigN0d9dFIH35W2fZBOkhvuLqOCDCBacIPjXPMxF7DRGyrz5lVHxTc04OlBeRX'
page_id = "screenless"
graph = facebook.GraphAPI(access_token)
print("uploading photo " + photo)
graph.put_photo(image=open(photo, 'rb'), album_path=page_id + "/photos",
message='#screenless')
@add_command("twtimeline", "Print Recent Tweets")
def tw_timeline(self, data=None):
"""
Print some recent tweets from your home timeline. Default is 10.
"""
if data:
try:
count = data["count"]
except KeyError as err:
print("You need to specify how many tweets you want!")
else:
count = 10
prn = self._get_small_printer()
# TODO: abstract this to use a simple templating system instead of raw
tweets = self.t.t.statuses.home_timeline(count=count,
tweet_mode="extended")
out = ""
for t in tweets:
prn.set(underline=1)
username = t["user"]["name"]
prn.textln(username)
prn.set()
if "retweeted_status" in t:
rt = t["retweeted_status"]
twtext = "RT from " + rt["user"]["name"] + "\n"
twtext += html.unescape(rt["full_text"])
else:
twtext = html.unescape(t["full_text"])
prn.block_text(twtext, font="0")
prn.ln()
if "media" in t["entities"]:
if len(t["entities"]["media"]) > 0:
i_url = t["entities"]["media"][0]["media_url"]
filename = i_url.rsplit('/',1)[1]
filename = "/tmp/" + filename
print("fetching", i_url, filename)
urllib.request.urlretrieve(i_url, filename)
im = PIL.Image.open(filename)
if im.mode in ("L", "RGB", "P"):
im = PIL.ImageOps.equalize(im)
im.thumbnail((self.smprint["width"], 960), PIL.Image.ANTIALIAS)
prn.image(im, impl="bitImageColumn")
tw_shortcode = self.short_tweet_id(t["id_str"])
#prn.barcode("PRtwd." + tw_shortcode, "CODE128", function_type="B")
#TODO: submit a patch to escpos to have a quiet_zone parameter
# as the default is a bit wide for 58mm printers
prn.soft_barcode("code128", "PRtwd." + tw_shortcode, module_width=0.16)
prn.ln(2)
prn.cut()
prn.close()
@add_command("twd", "Print Tweet Details")
def tw_details(self, data):
"""
Print detailed tweet info and commands for reply, like, retweet, etc.
"""
shortcode, _ = data.split(".")
tweet_id = self.get_tweet_id(shortcode)
tweet = self.t.t.statuses.show(id=tweet_id, tweet_mode="extended")
prn = self._get_small_printer()
prn.set(underline=1)
username = tweet["user"]["name"]
prn.textln(username)
prn.set()
if "retweeted_status" in tweet:
rt = tweet["retweeted_status"]
twtext = "RT from " + rt["user"]["name"] + "\n"
twtext += html.unescape(rt["full_text"])
else:
twtext = html.unescape(tweet["full_text"])
twtext += "\n"
prn.block_text(twtext, font="0")
if "media" in tweet["entities"]:
for entity in tweet["entities"]["media"]:
i_url = entity["media_url"]
filename = i_url.rsplit('/',1)[1]
filename = "/tmp/" + filename
print("fetching", i_url, filename)
urllib.request.urlretrieve(i_url, filename)
im = PIL.Image.open(filename)
if im.mode in ("L", "RGB", "P"):
im = PIL.ImageOps.equalize(im)
im.thumbnail((self.smprint["width"], 576), PIL.Image.ANTIALIAS)
prn.image(im, impl="bitImageColumn")
tw_shortcode = self.short_tweet_id(tweet["id_str"])
prn.text("Retweet\r\n")
#prn.barcode("PRtwrt." + tw_shortcode, "CODE128", function_type="B")
prn.soft_barcode("code128", "PRtwrt." + tw_shortcode, module_width=0.16)
prn.text("Like\r\n")
#prn.barcode("PRtwlk." + tw_shortcode, "CODE128", function_type="B")
prn.soft_barcode("code128", "PRtwlk." + tw_shortcode, module_width=0.16)
prn.text("Reply\r\n")
#prn.barcode("PRtwre." + tw_shortcode, "CODE128", function_type="B")
prn.soft_barcode("code128", "PRtwre." + tw_shortcode, module_width=0.16)
prn.ln(2)
prn.cut()
prn.close()
@add_command("twrt", "Re-Tweet")
def tw_retweet(self, data):
"""
Re-Tweet a tweet from someone else.
"""
shortcode, _ = data.split(".")
tweet_id = self.get_tweet_id(shortcode)
self.t.t.statuses.retweet(id=tweet_id)
@add_command("twre", "Reply to Tweet")
def tw_reply(self, data):
"""
Reply to a tweet.
"""
shortcode, _ = data.split(".")
tweet_id = self.get_tweet_id(shortcode)
tweet = self.t.t.statuses.show(id=tweet_id)
at_user = tweet["user"]["screen_name"]
self.tweet_pic(tweet_id=tweet_id, at_user=at_user)
@add_command("twlk", "Like a Tweet")
def tw_like(self, data):
"""
'Like' a tweet.
"""
shortcode, _ = data.split(".")
tweet_id = self.get_tweet_id(shortcode)
self.t.t.favorites.create(_id=tweet_id)
@add_command("mare", "Boost a toot")
def boost_toot(self, data):
"""
Boost a toot (or whatever kind of Fediverse content)
"""
shortcode, _ = data.split(".")
toot_id = self.get_tweet_id(shortcode)
self.masto.status_reblog(toot_id)
@add_command("mafv", "Favorite a toot")
def fav_toot(self, data):
"""
favorite a toot (or other kind of Fediverse content)
"""
shortcode, _ = data.split(".")
toot_id = self.get_tweet_id(shortcode)
self.masto.status_favorite(toot_id)
@add_command("marp", "Reply to a toot")
def reply_toot(self, data):
"""
Reply to a toot with the image under the document camera.
"""
shortcode, _ = data.split(".")
toot_id = self.get_tweet_id(shortcode)
self.toot_pic(reply_to=toot_id)
@add_command("toot", "Post a Document Camera Image to the Fediverse")
def toot_pic(self, reply_to=None):
"""
Takes a photograph using the document camera and posts it to the Fediverse (Mastodon, Pleroma, etc.)
"""
# TODO: maybe add @ocrbot mention or even better run OCR locally
# and use that as status and description
photo = self.send("PX", "photo")["photo"]
media = self.masto.media_post(photo)
post = self.masto.status_post("", in_reply_to_id=reply_to, media_ids=[media])
@add_command("mad", "Print Detailed Toot")
def showtoot(self, data):
"""
Prints out a detailed version of a fediverse post with all media. Allows
various social and public relations management.
"""
shortcode, _ = data.split(".")
toot_id = self.get_tweet_id(shortcode)
t = self.masto.status(toot_id)
prn = self._get_small_printer()
prn.set(underline=1)
print("toot from:" + str(t.account))
username = str(t.account.display_name)
acct = str(t.account.acct)
prn.textln(username + " (" + acct + ")")
prn.set()
ttext = bleach.clean(t.content, tags=[], strip=True)
ttext = html.unescape(ttext)
prn.block_text(ttext, font="0")
prn.ln()
for media in t.media_attachments:
if media.type == "image":
req_data = requests.get(media.url)
im = PIL.Image.open(io.BytesIO(req_data.content))
if im.mode in ("L", "RGB", "P"):
im = PIL.ImageOps.equalize(im)
im.thumbnail((self.smprint["width"], 960), PIL.Image.ANTIALIAS)
prn.image(im, impl="bitImageColumn")
prn.textln("Boost")
prn.soft_barcode("code128", "PRmare." + shortcode, module_width=0.16)
prn.textln("Favorite")
prn.soft_barcode("code128", "PRmafv." + shortcode, module_width=0.16)
prn.textln("Reply")
prn.soft_barcode("code128", "PRmad." + shortcode, module_width=0.16)
prn.ln(2)
prn.cut()
prn.close()
@add_command("tootline", "Print Recent Toots")
def tootline(self, data=None):
"""
Print some recent entries from your Fediverse timeline. Default is 10.
"""
if data:
try:
count = data["count"]
except KeyError as err:
print("You need to specify how many toots you want!")
else:
count = 10
prn = self._get_small_printer()
# TODO: add fancier formatting i.e. inverted text for username/handle
# TODO: clean this up to use the built in formatting from escpos lib
# and make a print_status function to use for notifications too
self.masto.debug_requests = True
toots = self.masto.timeline()
self.masto.debug_requests = False
out = ""
for t in toots:
prn.set(underline=1)
username = str(t.account.display_name)
acct = str(t.account.acct)
prn.textln(username + " (" + acct + ")")
prn.set()
ttext = bleach.clean(t.content, tags=[], strip=True)
ttext = html.unescape(ttext)
prn.block_text(ttext, font="0")
prn.ln()
if len(t.media_attachments) > 0:
img = None
t.media_attachments.reverse()
for media in t.media_attachments:
if media.type == "image":
img = media
if img:
req_data = requests.get(img.url)
im = PIL.Image.open(io.BytesIO(req_data.content))
if im.mode in ("L", "RGB", "P"):
im = PIL.ImageOps.equalize(im)
im.thumbnail((self.smprint["width"], 960), PIL.Image.ANTIALIAS)
prn.image(im, impl="bitImageColumn")
shortcode = self.short_tweet_id(str(t["id"]))
#prn.barcode("PRmad." + tw_shortcode, "CODE128", function_type="B")
prn.soft_barcode("code128", "PRmad." + shortcode, module_width=0.16)
with self.dbenv.begin(db=self.tweetdb, write=True) as txn:
txn.put(shortcode.encode(), str(t.id).encode())
notifications = self.masto.notifications(since_id=self.last_mast_notif)
if len(notifications) > 0:
prn.set(bold=True)
prn.textln("NOTIFICATIONS:")
prn.set()
# store the last notification id
self.last_mast_notif = str(notifications[0].id)
with self.dbenv.begin(db=self.tweetdb, write=True) as txn:
txn.put(b"last_mast_notif", self.last_mast_notif.encode())
for note in notifications:
username = note.account.display_name + " (" + note.account.acct + ")"
prn.text(note["type"] + " " + str(note["created_at"]) + " from ")
prn.textln(username + ":")
prn.textln(str(note["status"]))
prn.ln(2)
prn.cut()
prn.close()
def main():
pr = PublicRelations()
pr.run()
if __name__ == "__main__":
main()