diff --git a/auto-selfcontrol3 b/auto-selfcontrol3
new file mode 100644
index 0000000..619c81e
--- /dev/null
+++ b/auto-selfcontrol3
@@ -0,0 +1,274 @@
+#!/usr/bin/env python3.5.2
+
+import subprocess
+import os
+import json
+import datetime
+import syslog
+import traceback
+import sys
+from Foundation import NSUserDefaults, CFPreferencesSetAppValue, CFPreferencesAppSynchronize, NSDate
+from pwd import getpwnam
+from optparse import OptionParser
+
+
+def load_config(config_files):
+ """ loads json configuration files
+ the latter configs overwrite the previous configs
+ """
+
+ config = dict()
+
+ for f in config_files:
+ try:
+ with open(f, 'rt') as cfg:
+ config.update(json.load(cfg))
+ except ValueError as e:
+ exit_with_error("The json config file {configfile} is not correctly formatted." \
+ "The following exception was raised:\n{exc}".format(configfile=f, exc=e))
+
+ return config
+
+def run(config):
+ """ starts self-control with custom parameters, depending on the weekday and the config """
+
+ if check_if_running(config["username"]):
+ syslog.syslog(syslog.LOG_ALERT, "SelfControl is already running, ignore current execution of Auto-SelfControl.")
+ exit(2)
+
+ try:
+ schedule = next(s for s in config["block-schedules"] if is_schedule_active(s))
+ except StopIteration:
+ syslog.syslog(syslog.LOG_ALERT, "No schedule is active at the moment. Shutting down.")
+ exit(0)
+
+ duration = get_duration_minutes(schedule["end-hour"], schedule["end-minute"])
+
+ set_selfcontrol_setting("BlockDuration", duration, config["username"])
+ set_selfcontrol_setting("BlockAsWhitelist", 1 if schedule.get("block-as-whitelist", False) else 0,
+ config["username"])
+
+ if schedule.get("host-blacklist", None) is not None:
+ set_selfcontrol_setting("HostBlacklist", schedule["host-blacklist"], config["username"])
+ elif config.get("host-blacklist", None) is not None:
+ set_selfcontrol_setting("HostBlacklist", config["host-blacklist"], config["username"])
+
+ # In legacy mode manually set the BlockStartedDate, this should not be required anymore in future versions
+ # of SelfControl.
+ if config.get("legacy-mode", True):
+ set_selfcontrol_setting("BlockStartedDate", NSDate.date(), config["username"])
+
+ # Start SelfControl
+ os.system("{path}/Contents/MacOS/org.eyebeam.SelfControl {userId} --install".format(path=config["selfcontrol-path"], userId=str(getpwnam(config["username"]).pw_uid)))
+
+ syslog.syslog(syslog.LOG_ALERT, "SelfControl started for {min} minute(s).".format(min=duration))
+
+
+def check_if_running(username):
+ """ checks if self-control is already running. """
+ defaults = get_selfcontrol_settings(username)
+ return "BlockStartedDate" in defaults and not NSDate.distantFuture().isEqualToDate_(defaults["BlockStartedDate"])
+
+
+def is_schedule_active(schedule):
+ """ checks if we are right now in the provided schedule or not """
+ currenttime = datetime.datetime.today()
+ starttime = datetime.datetime(currenttime.year, currenttime.month, currenttime.day, schedule["start-hour"],
+ schedule["start-minute"])
+ endtime = datetime.datetime(currenttime.year, currenttime.month, currenttime.day, schedule["end-hour"],
+ schedule["end-minute"])
+ d = endtime - starttime
+
+ for weekday in get_schedule_weekdays(schedule):
+ weekday_diff = currenttime.isoweekday() % 7 - weekday % 7
+
+ if weekday_diff == 0:
+ # schedule's weekday is today
+ result = starttime <= currenttime and endtime >= currenttime if d.days == 0 else starttime <= currenttime
+ elif weekday_diff == 1 or weekday_diff == -6:
+ # schedule's weekday was yesterday
+ result = d.days != 0 and currenttime <= endtime
+ else:
+ # schedule's weekday was on any other day.
+ result = False
+
+ if result:
+ return result
+
+ return False
+
+
+def get_duration_minutes(endhour, endminute):
+ """ returns the minutes left until the schedule's end-hour and end-minute are reached """
+ currenttime = datetime.datetime.today()
+ endtime = datetime.datetime(currenttime.year, currenttime.month, currenttime.day, endhour, endminute)
+ d = endtime - currenttime
+ return int(round(d.seconds / 60.0))
+
+
+def get_schedule_weekdays(schedule):
+ """ returns a list of weekdays the specified schedule is active """
+ return [schedule["weekday"]] if schedule.get("weekday", None) is not None else range(1, 8)
+
+
+def set_selfcontrol_setting(key, value, username):
+ """ sets a single default setting of SelfControl for the provied username """
+ NSUserDefaults.resetStandardUserDefaults()
+ originalUID = os.geteuid()
+ os.seteuid(getpwnam(username).pw_uid)
+ CFPreferencesSetAppValue(key, value, "org.eyebeam.SelfControl")
+ CFPreferencesAppSynchronize("org.eyebeam.SelfControl")
+ NSUserDefaults.resetStandardUserDefaults()
+ os.seteuid(originalUID)
+
+
+def get_selfcontrol_settings(username):
+ """ returns all default settings of SelfControl for the provided username """
+ NSUserDefaults.resetStandardUserDefaults()
+ originalUID = os.geteuid()
+ os.seteuid(getpwnam(username).pw_uid)
+ defaults = NSUserDefaults.standardUserDefaults()
+ defaults.addSuiteNamed_("org.eyebeam.SelfControl")
+ defaults.synchronize()
+ result = defaults.dictionaryRepresentation()
+ NSUserDefaults.resetStandardUserDefaults()
+ os.seteuid(originalUID)
+ return result
+
+
+def get_launchscript(config):
+ """ returns the string of the launchscript """
+ return '''
+
+
+
+ Label
+ com.parrot-bytes.auto-selfcontrol
+ ProgramArguments
+
+ /usr/bin/python
+ {path}
+ -r
+
+ StartCalendarInterval
+
+ {startintervals}
+ RunAtLoad
+
+
+ '''.format(path=os.path.realpath(__file__), startintervals="".join(get_launchscript_startintervals(config)))
+
+
+def get_launchscript_startintervals(config):
+ """ returns the string of the launchscript start intervals """
+ entries = list()
+ for schedule in config["block-schedules"]:
+ for weekday in get_schedule_weekdays(schedule):
+ yield ('''
+ Weekday
+ {weekday}
+ Minute
+ {startminute}
+ Hour
+ {starthour}
+
+ '''.format(weekday=weekday, startminute=schedule['start-minute'], starthour=schedule['start-hour']))
+
+
+def install(config):
+ """ installs auto-selfcontrol """
+ print("> Start installation of Auto-SelfControl")
+
+ launchplist_path = "/Library/LaunchDaemons/com.parrot-bytes.auto-selfcontrol.plist"
+
+ # Check for existing plist
+ if os.path.exists(launchplist_path):
+ print("> Removed previous installation files")
+ subprocess.call(["launchctl", "unload", "-w", launchplist_path])
+ os.unlink(launchplist_path)
+
+ launchplist_script = get_launchscript(config)
+
+ with open(launchplist_path, 'w') as myfile:
+ myfile.write(launchplist_script)
+
+ subprocess.call(["launchctl", "load", "-w", launchplist_path])
+
+ print("> Installed\n")
+
+
+def check_config(config):
+ """ checks whether the config file is correct """
+ if not "username" in config:
+ exit_with_error("No username specified in config.")
+ if config["username"] not in get_osx_usernames():
+ exit_with_error(
+ "Username '{username}' unknown.\nPlease use your OSX username instead.\n" \
+ "If you have trouble finding it, just enter the command 'whoami'\n" \
+ "in your terminal.".format(
+ username=config["username"]))
+ if not "selfcontrol-path" in config:
+ exit_with_error("The setting 'selfcontrol-path' is required and must point to the location of SelfControl.")
+ if not os.path.exists(config["selfcontrol-path"]):
+ exit_with_error(
+ "The setting 'selfcontrol-path' does not point to the correct location of SelfControl. " \
+ "Please make sure to use an absolute path and include the '.app' extension, " \
+ "e.g. /Applications/SelfControl.app")
+ if "block-schedules" not in config:
+ exit_with_error("The setting 'block-schedules' is required.")
+ if len(config["block-schedules"]) == 0:
+ exit_with_error("You need at least one schedule in 'block-schedules'.")
+ if config.get("host-blacklist", None) is None:
+ print("WARNING:")
+ msg = "It is not recommended to directly use SelfControl's blacklist. Please use the 'host-blacklist' " \
+ "setting instead."
+ print(msg)
+ syslog.syslog(syslog.LOG_WARNING, msg)
+
+
+def get_osx_usernames():
+ output = subprocess.check_output(["dscl", ".", "list", "/users"])
+ return [s.strip().decode("utf-8") for s in output.splitlines()]
+
+
+def excepthook(excType, excValue, tb):
+ """ this function is called whenever an exception is not caught """
+ err = "Uncaught exception:\n{}\n{}\n{}".format(str(excType), excValue,
+ "".join(traceback.format_exception(excType, excValue, tb)))
+ syslog.syslog(syslog.LOG_CRIT, err)
+ print(err)
+
+
+def exit_with_error(message):
+ syslog.syslog(syslog.LOG_CRIT, message)
+ print("ERROR:")
+ print(message)
+ exit(1)
+
+
+if __name__ == "__main__":
+ __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
+ sys.excepthook = excepthook
+
+ syslog.openlog("Auto-SelfControl")
+
+ if os.geteuid() != 0:
+ exit_with_error("Please make sure to run the script with elevated rights, such as:\nsudo python {file}".format(
+ file=os.path.realpath(__file__)))
+
+ parser = OptionParser()
+ parser.add_option("-r", "--run", action="store_true",
+ dest="run", default=False)
+ (opts, args) = parser.parse_args()
+ config = load_config([os.path.join(__location__, "config.json")])
+
+ if opts.run:
+ run(config)
+ else:
+ check_config(config)
+ install(config)
+ if not check_if_running(config["username"]) and any(s for s in config["block-schedules"] if is_schedule_active(s)):
+ print("> Active schedule found for SelfControl!")
+ print("> Start SelfControl (this could take a few minutes)\n")
+ run(config)
+ print("\n> SelfControl was started.\n")