-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmain.py
221 lines (171 loc) · 7.06 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import json
import logging
import os
import secrets
import string
import sys
from datetime import datetime
from os import environ
from typing import Dict, NoReturn
from dateutil import tz
# Add the src directory to the module search path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
from crontab import CronTab
from src import logger_config, post_list
def log_and_exit(logger: logging.Logger, message: str) -> NoReturn:
"""
Log an error message and exit the program.
Args:
- logger (logging.Logger): The logger to use.
- message (str): The error message to log.
"""
logger.error(message)
sys.exit(1)
def get_shell_script_to_run(
user_shell: str, current_dir: str, logger: logging.Logger
) -> str:
"""
Determine the script to run based on the user's shell.
Args:
- user_shell (str): The user's shell.
- current_dir (str): The current directory of the script.
- logger (logging.Logger): The logger to use.
Returns:
- str: The path to the appropriate shell script for the user's shell.
Raises:
- SystemExit: If the user's shell is unsupported.
"""
shell_script_map: Dict[str, str] = {
"bash": os.path.join(current_dir, "src", "scripts", "run_media_post.sh"),
"fish": os.path.join(current_dir, "src", "scripts", "run_media_post.fish"),
}
run_media_post_path = shell_script_map.get(user_shell, None)
if run_media_post_path is None:
log_and_exit(logger=logger, message=f"Unsupported shell: {user_shell}")
return run_media_post_path
def validate_post_date(post_date: str, logger: logging.Logger) -> datetime:
"""
Validate the post date to ensure it is in the future.
Args:
- post_date (string): The date and time of the post.
- logger (logging.Logger): The logger to use.
Returns:
- datetime: The validated and parsed datetime object.
Raises:
- SystemExit: If the post date is not valid or not in the future.
"""
# Define the expected format for parsing
date_format = "%Y-%m-%d %H:%M"
try:
# Attempt to parse the post_date string into a datetime object
parsed_date = datetime.strptime(post_date, date_format)
except ValueError:
log_and_exit(
logger=logger,
message=f"The post_date is not in the correct format: {post_date}",
)
# Check if the parsed date is in the future
if parsed_date.astimezone(tz.UTC) <= datetime.now(tz=tz.UTC):
log_and_exit(
logger=logger, message=f"The post_date `{post_date}` is in the past."
)
return parsed_date
def create_cron_job(
cron: CronTab,
user_shell: str,
run_media_post_path: str,
media_post_path: str,
scheduled_post_file_path: str,
post_date: datetime,
logger: logging.Logger,
) -> None:
"""
Create a cron job for a scheduled post.
Args:
- cron (CronTab): The crontab object for the current user.
- user_shell (str): The user's shell.
- run_media_post_path (str): The path to the shell script to run.
- media_post_path (str): The path to the media post script.
- scheduled_post_file_path (str): The path to the scheduled post file.
- post_date (datetime): The date and time to run the job.
- logger (logging.Logger): The logger to use.
Raises:
- SystemExit: If the cron job creation fails.
"""
try:
# Conditionally add semicolon
command = (
f"SHELL=$(command -v {user_shell})"
+ (";" if user_shell == "bash" else "")
+ f" {user_shell} {run_media_post_path} {media_post_path} {scheduled_post_file_path}"
)
job = cron.new(command=command)
job.setall(post_date.strftime("%M %H %d %m *"))
except Exception as e:
log_and_exit(logger=logger, message=f"Failed to create cron job: {e}")
def main() -> None:
"""
Main function to schedule Instagram posts using cron jobs.
This function performs the following tasks:
1. Sets up logging to a file.
2. Loads a list of posts from a JSON file.
3. Creates a temporary JSON file for each post to be scheduled.
4. Schedules a cron job to execute a script for each post at the specified date and time.
5. Writes the cron jobs to the user's crontab.
The cron job will execute the script `media_post.py` with the path to the temporary JSON file as an argument.
"""
# Determine the current directory of the script
current_dir = os.path.dirname(os.path.abspath(__file__))
# Define paths for log file and posts JSON file
log_path = os.path.join(current_dir, "logs", "post-activity.log")
to_post_path = os.path.join(current_dir, "data", "to-post.json")
media_post_path = os.path.join(current_dir, "src", "media_post.py")
# Initialize logger
logger = logger_config.get_logger(log_file=log_path)
post_data_dir = os.path.join(current_dir, "data", "scheduled_posts")
os.makedirs(post_data_dir, exist_ok=True)
# Initialize PostList object and load posts from JSON file
posts_list = post_list.PostList(log_path)
posts_list.get_posts_from_json_file(posts_file_path=to_post_path)
logger.info(f"Number of posts loaded: {len(posts_list.posts)}")
user_shell = os.path.basename(environ.get("SHELL", "/bin/bash"))
run_media_post_path = get_shell_script_to_run(
user_shell=user_shell, current_dir=current_dir, logger=logger
)
# Access the current user's CronTab object.
cron = CronTab(user=True)
for post in posts_list.posts:
# Create a unique identifier for each post file
unique_id = "".join(
secrets.choice(string.ascii_lowercase + string.digits) for _ in range(6)
)
post.post_date = validate_post_date(post_date=post.post_date, logger=logger)
# Create a unique suffix for the temporary file based on the post date
post_date_suffix = post.post_date.strftime("%Y-%m-%d-%H-%M")
scheduled_post_file_path = os.path.join(
post_data_dir, f"insta_post_{unique_id}_{post_date_suffix}.json"
)
# Write the post data to the temporary file
try:
with open(scheduled_post_file_path, "w") as f:
json.dump(post.serialize(), f, default=str)
except (IOError, json.JSONDecodeError) as e:
log_and_exit(logger=logger, message=f"Failed to write post file: {e}")
# Create a new cron job to run the Instagram post script with the temp file as an argument
create_cron_job(
cron=cron,
user_shell=user_shell,
run_media_post_path=run_media_post_path,
media_post_path=media_post_path,
scheduled_post_file_path=scheduled_post_file_path,
post_date=post.post_date,
logger=logger,
)
# Write the cron jobs to the user's crontab
try:
cron.write()
logger.info(f"Cronjob added to the CronTab for the current user: {cron.user}")
except Exception as e:
log_and_exit(logger=logger, message=f"Failed to write to CronTab: {e}")
if __name__ == "__main__":
main()