├── README.md ├── auth.py ├── main.py └── utils.py /README.md: -------------------------------------------------------------------------------- 1 | # x-twitter-bot 2 | 3 | See https://youtu.be/veheaaMRS_Q for a walkthrough of the bot / code 4 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | import tweepy 2 | from keys import api_key, api_secret, access_token, access_token_secret 3 | 4 | def get_twitter_conn_v1() -> tweepy.API: 5 | """Get twitter conn 1.1""" 6 | 7 | auth = tweepy.OAuth1UserHandler(api_key, api_secret) 8 | auth.set_access_token( 9 | access_token, 10 | access_token_secret, 11 | ) 12 | return tweepy.API(auth) 13 | 14 | def get_twitter_conn_v2() -> tweepy.Client: 15 | """Get twitter conn 2.0""" 16 | 17 | client = tweepy.Client( 18 | consumer_key=api_key, 19 | consumer_secret=api_secret, 20 | access_token=access_token, 21 | access_token_secret=access_token_secret, 22 | ) 23 | 24 | return client 25 | 26 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from apscheduler.schedulers.blocking import BlockingScheduler 2 | import time 3 | from datetime import datetime, timedelta 4 | 5 | from auth import get_twitter_conn_v1, get_twitter_conn_v2 6 | from utils import create_and_post_tweet 7 | 8 | # Authenticate to Twitter 9 | client_v1 = get_twitter_conn_v1() 10 | client_v2 = get_twitter_conn_v2() 11 | 12 | # List of specific times when you want the tweet to be posted (in Eastern Time) 13 | post_times_et = [(6, 0), (18, 0)] 14 | 15 | def job(): 16 | # Get the current time in UTC 17 | now_utc = datetime.utcnow() 18 | 19 | # Convert UTC time to EST (Eastern Standard Time) by subtracting 5 hours 20 | now_est = now_utc - timedelta(hours=4) 21 | 22 | print(f"job run at {now_est}") 23 | 24 | # Check if the current time falls within a 1-minute range of any of the post_times_et 25 | for post_time_hour, post_time_minute in post_times_et: 26 | if now_est.hour == post_time_hour and (now_est.minute - post_time_minute) == 1: 27 | # If it's within the 1-minute range of the specified time, post the tweet 28 | create_and_post_tweet(client_v1, client_v2) 29 | 30 | 31 | if __name__ == '__main__': 32 | # Initialize the scheduler 33 | scheduler = BlockingScheduler() 34 | 35 | # Schedule the job to run every minute 36 | scheduler.add_job(job, 'interval', minutes=1) 37 | 38 | # Start the scheduler 39 | scheduler.start() 40 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import requests 4 | from keys import google_key, cx 5 | 6 | # Assuming utils.py is inside the app folder 7 | APP_DIR = os.path.dirname(__file__) 8 | 9 | # Path to the tweet counter file (inside the /app directory) 10 | TWEET_COUNTER_FILE = os.path.join(APP_DIR, "tweet_counter.txt") 11 | 12 | # Path to the SQLite database file (inside the /app directory) 13 | DB_FILE_PATH = os.path.join(APP_DIR, "my_database.db") 14 | 15 | # Path to the jpg file (inside the /app directory) 16 | TEMP_IMAGE_DIR = os.path.join(APP_DIR, "temp_image.jpg") 17 | 18 | # Path to Tweet Order Dict txt file (inside the /app directory) 19 | TWEET_ORDER_DICT = os.path.join(APP_DIR, "tweet_order_dict.txt") 20 | 21 | def create_image_link(search, attempt): 22 | 23 | base_url = "https://www.googleapis.com/customsearch/v1" 24 | params = { 25 | "key": google_key, 26 | "cx": cx, 27 | "q": search, 28 | "num": attempt, 29 | "searchType": 'image' 30 | } 31 | 32 | response = requests.get(base_url, params=params) 33 | data = response.json() 34 | 35 | index = attempt - 1 36 | 37 | if 'items' in data: 38 | items_dict = data['items'][index] 39 | if items_dict['link']: 40 | return items_dict['link'] 41 | elif items_dict['image']: 42 | image_dict = items_dict['image'] 43 | return image_dict['contextLink'] 44 | else: 45 | print("No image link") 46 | elif 'error' in data: 47 | print(data['error']) 48 | else: 49 | print("No 'ITEMS' in JSON response") 50 | 51 | 52 | def save_api_requests(): 53 | return "https://www.christies.com/img/LotImages/2006/NYR/2006_NYR_01717_0118_000(125845).jpg?mode=max" 54 | 55 | 56 | def get_search(id): 57 | # Connect to the SQLite database 58 | conn = sqlite3.connect(DB_FILE_PATH) 59 | 60 | try: 61 | # Query the database to get the search 62 | cursor = conn.cursor() 63 | cursor.execute("SELECT art_title, artist FROM tweets WHERE id=?", (id,)) 64 | row_data = cursor.fetchone() 65 | art_title = row_data[0] 66 | artist = row_data[1] 67 | search = f"{art_title} by {artist}" 68 | return search 69 | 70 | except sqlite3.Error as e: 71 | print("Error occurred while querying the database:", e) 72 | return None 73 | 74 | finally: 75 | # Close the connection to the database 76 | conn.close() 77 | 78 | 79 | def fetch_tweet_data(id): 80 | # Connect to the SQLite database 81 | conn = sqlite3.connect(DB_FILE_PATH) 82 | 83 | try: 84 | # Query the database to get the tweet data based on the id 85 | cursor = conn.cursor() 86 | cursor.execute("SELECT art_title, artist, year, description FROM tweets WHERE id=?", (id,)) 87 | row_data = cursor.fetchone() 88 | 89 | if row_data: 90 | # Convert the row data to a dictionary 91 | tweet_data = { 92 | 'art_title': row_data[0], 93 | 'artist': row_data[1], 94 | 'year': row_data[2], 95 | 'description': row_data[3] 96 | } 97 | return tweet_data 98 | else: 99 | print(f"No data found for id: {id}.") 100 | return None 101 | 102 | except sqlite3.Error as e: 103 | print("Error occurred while querying the database:", e) 104 | return None 105 | 106 | finally: 107 | # Close the connection to the database 108 | conn.close() 109 | 110 | def load_tweet_order_dict(file_path): 111 | if os.path.exists(file_path): 112 | with open(file_path, "r") as file: 113 | return {int(k): int(v) for k, v in [line.strip().split(',') for line in file]} 114 | else: 115 | return {} 116 | 117 | def create_and_post_tweet(client_v1, client_v2): 118 | max_attempts = 7 119 | 120 | # Load the tweet order dictionary from the file 121 | tweet_order_dict = load_tweet_order_dict(TWEET_ORDER_DICT) 122 | 123 | # Read the current tweet counter from the file 124 | if os.path.exists(TWEET_COUNTER_FILE): 125 | with open(TWEET_COUNTER_FILE, "r") as counter_file: 126 | tweet_counter = int(counter_file.read()) 127 | else: 128 | tweet_counter = 1 # Start with 1 if the file doesn't exist 129 | 130 | while tweet_counter: 131 | # Fetch tweet data based on the current tweet counter from the tweet_order_dict 132 | tweet_id = tweet_order_dict[tweet_counter] 133 | tweet_data = fetch_tweet_data(tweet_id) 134 | if tweet_data is None: 135 | print("No more tweets to post.") 136 | return 137 | 138 | art_title = tweet_data['art_title'] 139 | artist = tweet_data['artist'] 140 | year = tweet_data['year'] 141 | description = tweet_data['description'] 142 | 143 | # Check if tweet_text needs to be split into multiple tweets 144 | tweet_text = f"{art_title} by {artist}\nDate: {year}\n\n{description}" 145 | desc = None 146 | if len(tweet_text) > 280: 147 | tweet_text1 = f"{art_title} by {artist}\n\nDate: {year}" 148 | desc = f"{description}" 149 | 150 | attempt = 1 151 | search = get_search(tweet_id) 152 | image_link = create_image_link(search, attempt) 153 | 154 | # Download the image from the image link 155 | while attempt <= max_attempts: 156 | try: 157 | if 'wiki' in image_link: 158 | headers = {'User-Agent': 'ArtBot ()'} # Add information Here, See https://meta.wikimedia.org/wiki/User-Agent_policy 159 | response = requests.get(image_link, headers=headers) 160 | else: 161 | response = requests.get(image_link) 162 | 163 | response.raise_for_status() 164 | 165 | # Save the image to a local file (e.g., 'temp_image.jpg') 166 | with open(TEMP_IMAGE_DIR, 'wb') as f: 167 | f.write(response.content) 168 | 169 | media_path = TEMP_IMAGE_DIR 170 | media = client_v1.simple_upload(filename=media_path) 171 | media_id = media.media_id 172 | 173 | # Post the tweet with the image and tweet_text 174 | if desc is not None: 175 | tweet = client_v2.create_tweet(media_ids=[media_id], text=tweet_text1) 176 | client_v2.create_tweet(text=desc, in_reply_to_tweet_id=tweet.data['id']) 177 | else: 178 | client_v2.create_tweet(text=tweet_text, media_ids=[media_id]) 179 | 180 | print(f"Success - Tweet Number: {tweet_counter}, Tweet ID: {tweet_id}, Title: {search}, has been posted") 181 | 182 | # Delete the image file after tweeting 183 | os.remove(TEMP_IMAGE_DIR) 184 | 185 | # Increment the tweet counter 186 | tweet_counter += 1 187 | 188 | # Check if the tweet counter has reached the maximum value 189 | if tweet_counter > len(tweet_order_dict): 190 | # If so, reset the tweet counter to 1 and save the current tweet ID to the file 191 | tweet_counter = 1 192 | with open(TWEET_COUNTER_FILE, "w") as counter_file: 193 | counter_file.write(str(tweet_counter)) 194 | 195 | else: 196 | # Save the updated tweet counter to the file 197 | with open(TWEET_COUNTER_FILE, "w") as counter_file: 198 | counter_file.write(str(tweet_counter)) 199 | 200 | return 201 | 202 | except Exception as e: 203 | # Exception occurred while downloading image or uploading media 204 | print(f"Error: {e}") 205 | if os.path.exists(TEMP_IMAGE_DIR): 206 | os.remove(TEMP_IMAGE_DIR) # Remove the invalid image file 207 | 208 | print(f"Attempt {attempt} of 7 failed for Tweet Number: {tweet_counter} - Tweet ID: {tweet_id}, generating new image") 209 | 210 | # Attempt to generate a new image link and continue with the next attempt 211 | attempt += 1 212 | image_link = create_image_link(search, attempt) 213 | 214 | else: 215 | print(f"Maximum attempts ({max_attempts}) reached for tweet {tweet_counter}.") 216 | tweet_counter += 1 # Move to the next tweet and attempt to post it 217 | 218 | # Save the updated tweet counter to the file 219 | with open(TWEET_COUNTER_FILE, "w") as counter_file: 220 | counter_file.write(str(tweet_counter)) 221 | 222 | --------------------------------------------------------------------------------