From 9e8fa9ccb16c3a97fd277710b47f06f4d8d5ed31 Mon Sep 17 00:00:00 2001 From: spla Date: Thu, 7 Oct 2021 20:23:17 +0200 Subject: [PATCH] Added thread support --- README.md | 9 ++- db-setup.py | 4 + mastodon-setup.py | 190 ++++++++++++++++++++++++++++++++++++++++++++++ mastotuit.py | 125 ++++++++++++++++++++++++++---- 4 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 mastodon-setup.py diff --git a/README.md b/README.md index fee8b31..fe01146 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ Within Python Virtual Environment: 2. Run `python db-setup.py` to setup and create new Postgresql database and needed table in it and setup your Mastodon's account RSS feed in the format 'https://your.mastodon.server/@your_user.rss' -3. Run `python setup.py` to input and save your Twitter's key and access tokens. You can get your keys and tokens from [Twitter Developer Platform](https://developer.twitter.com/en/apply/user.html) +3. Run `python setup.py` to input and save your Twitter's key and access tokens. You can get your keys and tokens from [Twitter Developer Platform](https://developer.twitter.com/en/apply/user.html) -4. Use your favourite scheduling method to set `python mastotuit.py` to run every minute. +4. Run `python mastodon-setup.py` to setup your Mastodon account access tokens. -29.9.2021 **New Feature** Added support to media files! mastotuit now gets all media files from Mastodon's post (if any) and publish them to Twitter together with your status update. +5. Use your favourite scheduling method to set `python mastotuit.py` to run every minute. + +29.9.2021 **New Feature** Added support to media files! mastotuit now gets all media files from Mastodon's post (if any) and publish them to Twitter together with your status update. +7.10.2021 **New Feature** Added thread support! If you create a thread in Mastodon mastotuit will create the same thread on Twitter. diff --git a/db-setup.py b/db-setup.py index 8b574bf..79d133a 100644 --- a/db-setup.py +++ b/db-setup.py @@ -139,6 +139,10 @@ table = "feeds" sql = "create table "+table+" (link varchar(300) PRIMARY KEY)" create_table(db, db_user, table, sql) +table = "id" +sql = "create table "+table+" (toot_id bigint PRIMARY KEY, tweet_id bigint)" +create_table(db, db_user, table, sql) + ##################################### print("Done!") diff --git a/mastodon-setup.py b/mastodon-setup.py new file mode 100644 index 0000000..f1ab563 --- /dev/null +++ b/mastodon-setup.py @@ -0,0 +1,190 @@ +import getpass +from mastodon import Mastodon +from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout, MastodonAPIError, MastodonIllegalArgumentError +import fileinput,re +import os +import sys + +def create_dir(): + if not os.path.exists('secrets'): + os.makedirs('secrets') + +def create_file(): + if not os.path.exists('secrets/secrets.txt'): + with open('secrets/secrets.txt', 'w'): pass + print(secrets_filepath + " created!") + +def create_config(): + if not os.path.exists('config'): + os.makedirs('config') + if not os.path.exists(config_filepath): + print(config_filepath + " created!") + with open('config/config.txt', 'w'): pass + +def write_params(): + with open(secrets_filepath, 'a') as the_file: + print("Writing secrets parameter names to " + secrets_filepath) + the_file.write('uc_client_id: \n'+'uc_client_secret: \n'+'uc_access_token: \n') + +def write_config(): + with open(config_filepath, 'a') as the_file: + the_file.write('mastodon_hostname: \n') + print("adding parameter name 'mastodon_hostname' to "+ config_filepath) + +def read_client_lines(self): + client_path = 'app_clientcred.txt' + with open(client_path) as fp: + line = fp.readline() + cnt = 1 + while line: + if cnt == 1: + print("Writing client id to " + secrets_filepath) + modify_file(secrets_filepath, "uc_client_id: ", value=line.rstrip()) + elif cnt == 2: + print("Writing client secret to " + secrets_filepath) + modify_file(secrets_filepath, "uc_client_secret: ", value=line.rstrip()) + line = fp.readline() + cnt += 1 + +def read_token_line(self): + token_path = 'app_usercred.txt' + with open(token_path) as fp: + line = fp.readline() + print("Writing access token to " + secrets_filepath) + modify_file(secrets_filepath, "uc_access_token: ", value=line.rstrip()) + +def read_config_line(): + with open(config_filepath) as fp: + line = fp.readline() + modify_file(config_filepath, "mastodon_hostname: ", value=hostname) + +def log_in(): + error = 0 + try: + global hostname + hostname = input("Enter Mastodon hostname: ") + user_name = input("User name, ex. user@" + hostname +"? ") + user_password = getpass.getpass("User password? ") + app_name = input("This app name? ") + Mastodon.create_app(app_name, scopes=["read","write"], + to_file="app_clientcred.txt", api_base_url=hostname) + mastodon = Mastodon(client_id = "app_clientcred.txt", api_base_url = hostname) + mastodon.log_in( + user_name, + user_password, + scopes = ["read", "write"], + to_file = "app_usercred.txt" + ) + except MastodonIllegalArgumentError as i_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(i_error) + except MastodonNetworkError as n_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(n_error) + except MastodonReadTimeout as r_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(r_error) + except MastodonAPIError as a_error: + error = 1 + if os.path.exists("secrets/secrets.txt"): + print("Removing secrets/secrets.txt file..") + os.remove("secrets/secrets.txt") + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt file..") + os.remove("app_clientcred.txt") + sys.exit(a_error) + finally: + if error == 0: + + create_dir() + create_file() + write_params() + client_path = 'app_clientcred.txt' + read_client_lines(client_path) + token_path = 'app_usercred.txt' + read_token_line(token_path) + if os.path.exists("app_clientcred.txt"): + print("Removing app_clientcred.txt temp file..") + os.remove("app_clientcred.txt") + if os.path.exists("app_usercred.txt"): + print("Removing app_usercred.txt temp file..") + os.remove("app_usercred.txt") + print("Secrets setup done!\n") + +def modify_file(file_name,pattern,value=""): + fh=fileinput.input(file_name,inplace=True) + for line in fh: + replacement=pattern + value + line=re.sub(pattern,replacement,line) + sys.stdout.write(line) + fh.close() + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if secrets file exists + if not os.path.isfile(file_path): + print("File %s not found, creating it."%file_path) + log_in() + + # Find parameter in file + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(file_path + " Missing parameter %s "%parameter) + sys.exit(0) + +# Returns the parameter from the specified file +def get_hostname( parameter, config_filepath ): + # Check if secrets file exists + if not os.path.isfile(config_filepath): + print("File %s not found, creating it."%config_filepath) + create_config() + + # Find parameter in file + with open( config_filepath ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(config_filepath + " Missing parameter %s "%parameter) + write_config() + read_config_line() + print("hostname setup done!") + sys.exit(0) + +############################################################################### +# main + +if __name__ == '__main__': + + # Load secrets from secrets file + secrets_filepath = "secrets/secrets.txt" + uc_client_id = get_parameter("uc_client_id", secrets_filepath) + uc_client_secret = get_parameter("uc_client_secret", secrets_filepath) + uc_access_token = get_parameter("uc_access_token", secrets_filepath) + + # Load configuration from config file + config_filepath = "config/config.txt" + mastodon_hostname = get_hostname("mastodon_hostname", config_filepath) # E.g., mastodon.social diff --git a/mastotuit.py b/mastotuit.py index 4a4db74..8fe4440 100644 --- a/mastotuit.py +++ b/mastotuit.py @@ -14,6 +14,40 @@ import pdb logger = logging.getLogger() +def get_tweet_id(toot_id): + + tweet_id = 0 + + try: + + conn = None + + conn = psycopg2.connect(database = feeds_db, user = feeds_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute('select tweet_id from id where toot_id=(%s)', (toot_id,)) + + row = cur.fetchone() + + if row != None: + + tweet_id = row[0] + + cur.close() + + return(tweet_id) + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + def write_image(image_url): if not os.path.exists('images'): @@ -109,12 +143,12 @@ def get_parameter( parameter, file_path ): if __name__ == '__main__': ####################################################################### - #mastodon, mastodon_hostname = mastodon() - + mastodon, mastodon_hostname = mastodon() + feeds_db, feeds_db_user, feeds_url = db_config() api_key, api_key_secret, access_token, access_token_secret = twitter_config() - + logged_in = False try: @@ -130,11 +164,19 @@ if __name__ == '__main__': publish = False with_images = False + is_reply = False title = entry['summary'] id = entry['id'] link = entry['link'] + toot_id = link.rsplit('/')[4] + reply_id = mastodon.status(toot_id).in_reply_to_id + if reply_id != None: + + is_reply = True + tweet_id = get_tweet_id(reply_id) + if len(entry.links) >= 2: with_images = True @@ -183,7 +225,7 @@ if __name__ == '__main__': if publish: soup = BeautifulSoup(title, 'html.parser') - + toot_text = soup.get_text() sub_str = 'http' @@ -218,7 +260,7 @@ if __name__ == '__main__': if not logged_in: api, logged_in = create_api() - + if len(tuit_text) < 280: try: @@ -234,17 +276,29 @@ if __name__ == '__main__': images_id_lst.append(media.media_id) i += 1 - api.update_status(status=tuit_text, media_ids=images_id_lst) + if is_reply: + + tweet = api.update_status(status=tuit_text, in_reply_to_status_id=tweet_id, media_ids=images_id_lst) + + else: + + tweet = api.update_status(status=tuit_text, media_ids=images_id_lst) else: - api.update_status(tuit_text) + if is_reply: + + tweet = api.update_status(status=tuit_text, in_reply_to_status_id=tweet_id) + + else: + + tweet = api.update_status(tuit_text) except TweepError as err: - + print('\n') sys.exit(err) - + else: if with_images: @@ -270,16 +324,30 @@ if __name__ == '__main__': images_id_lst.append(media.media_id) i += 1 - first_tweet = api.update_status(status=tuit_text1) - api.update_status(status=tuit_text2, in_reply_to_status_id=first_tweet.id, media_ids=images_id_lst) + if is_reply: + + first_tweet = api.update_status(status=tuit_text1, in_reply_to_status_id=tweet_id) + + else: + + first_tweet = api.update_status(status=tuit_text1) + + tweet = api.update_status(status=tuit_text2, in_reply_to_status_id=first_tweet.id, media_ids=images_id_lst) else: - first_tweet = api.update_status(tuit_text1) - api.update_status(tuit_text2, in_reply_to_status_id=first_tweet.id) - + if is_reply: + + first_tweet = api.update_status(tuit_text1, in_reply_to_status_id=tweet_id) + + else: + + first_tweet = api.update_status(tuit_text1) + + tweet = api.update_status(tuit_text2, in_reply_to_status_id=first_tweet.id) + except TweepError as err: - + print('\n') sys.exit(err) @@ -312,6 +380,33 @@ if __name__ == '__main__': if conn is not None: conn.close() + + insert_line = 'INSERT INTO id(toot_id, tweet_id) VALUES (%s,%s)' + + conn = None + + try: + + conn = psycopg2.connect(database = feeds_db, user = feeds_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute(insert_line, (toot_id, tweet.id)) + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + else: print("Any new feeds")