#!/Users/gl6/env/bin/python
import os
import json
import requests
import uuid
import browser_cookie3
import time
from datetime import datetime, timedelta
import re
import click
import logging
from rich.console import Console
from rich.theme import Theme
from rich.logging import RichHandler
STORAGE_FILE = os.path.join(os.path.expanduser('~'), '.wf.json')
INBOX_ID = "13859f14-79ab-b758-7224-8dad0236f1e2"
TASKS_ID = "042d7e11-f613-856c-fb97-c0e6ee7ece05"
DIARY_ID = "01205285-f5b1-8026-0cfa-942f514e297e"
PLANNER_IDS = {
"life": "f6f993a3-c696-c070-296e-6e5055fc834f",
"admin": "d3911123-138e-8eb3-f2ba-2495d8169660",
"art": "c2e9f4b2-59ce-d127-4da6-949e54ec1442",
"health": "4fc929b8-f04b-0dde-9a1a-9d889a13316d",
"mind": "c6db70a1-72e3-dbcc-f4b9-bfb10a1b4280",
"social": "60e3d667-eb40-3d26-cc3f-6b151cc5efa4",
"church": "5c68fad5-ad2b-8018-6535-b1462aed1277",
"work": "4c4970fc-9023-f861-1392-dbf88dd89187",
"affirm": "a1412d52-c72c-0612-f5b1-48874ef03943",
}
solarized_theme = Theme({
"base03": "bright_black",
"base02": "black",
"base01": "bright_green",
"base00": "bright_yellow",
"base0": "bright_blue",
"base1": "bright_cyan",
"base2": "white",
"base3": "bright_white",
"orange": "bright_red",
"violet": "bright_magenta",
"red": "red",
"bold red": "bold red",
"underline base03": "underline bright_black",
"underline base02": "underline black",
"underline base01": "underline bright_green",
"underline base00": "underline bright_yellow",
"underline base0": "underline bright_blue",
"underline base1": "underline bright_cyan",
"underline base2": "underline white",
"underline base3": "underline bright_white",
"underline orange": "bright_red underline",
"underline violet": "bright_magenta underline",
"bold base03": "bold bright_black",
"bold base02": "bold black",
"bold base01": "bold bright_green",
"bold base00": "bold bright_yellow",
"bold base0": "bold bright_blue",
"bold base1": "bold bright_cyan",
"bold base2": "bold white",
"bold base3": "bold bright_white",
"bold orange": "bright_red bold",
"bold violet": "bright_magenta bold",
})
console = Console(highlight=False, theme=solarized_theme)
logging.basicConfig(handlers=[RichHandler(level="NOTSET", console=console)])
logger = logging.getLogger('rich')
logger.setLevel(logging.INFO)
# helpers {{{
def get_ordinal(n):
if 10 <= n % 100 <= 20:
suffix = 'th'
else:
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
return str(n) + suffix
def get_today():
now = datetime.now()
return now.strftime("%a, %b %d, %Y")
# return now.strftime(f"%a {get_ordinal(now.day)} %b")
def get_sunday():
now = datetime.now()
sunday = now - timedelta(days=now.weekday()) - timedelta(days=1)
return sunday.strftime("%a, %b %d, %Y")
# return now.strftime(f"%a {get_ordinal(now.day)} %b")
def generate_uuid():
return str(uuid.uuid4())
def load_storage():
if os.path.exists(STORAGE_FILE):
logger.debug(f"Loading storage from {STORAGE_FILE}")
with open(STORAGE_FILE, "r") as f:
return json.load(f)
return {}
def save_storage(data):
with open(STORAGE_FILE, "w") as f:
json.dump(data, f)
def save_to_storage(key, value):
storage = load_storage()
storage[key] = value
save_storage(storage)
def load_from_storage(key):
storage = load_storage()
if key in storage:
return storage[key]
return {}
def clear_storage():
if os.path.exists(STORAGE_FILE):
os.remove(STORAGE_FILE)
# }}}
def refresh_cookie(): # {{{
logger.debug("Refreshing session cookie")
cookies = browser_cookie3.chrome()
session_cookie = None
for cookie in cookies:
if cookie.name == "sessionid" and "workflowy.com" in cookie.domain:
session_cookie = cookie.value
break
if session_cookie:
logger.debug(f"Found session cookie: {session_cookie}")
save_to_storage("session_cookie", session_cookie)
return True
else:
logger.error("Session cookie not found. Are you logged into Workflowy?")
return False
# }}}
def check_cookie(): # {{{
session_cookie = load_from_storage("session_cookie")
if session_cookie:
logger.debug(f"Session cookie found: {session_cookie}")
else:
logger.error("Session cookie not found. Run refresh_cookie() first.")
return False
url = "https://workflowy.com/get_initialization_data?client_version=15"
headers = {"Cookie": f"sessionid={session_cookie}"}
response = requests.get(url, headers=headers)
if response.status_code == 200:
logger.debug("Session cookie is valid.")
return True
else:
logger.error(f"Session cookie is invalid. Status code {response.status_code}")
return False
# }}}
def refresh_workflowy_data(): # {{{
session_cookie = load_from_storage("session_cookie")
if not session_cookie:
console.log("Session cookie not found. Run refresh_cookie() first.")
return
url = "https://workflowy.com/get_initialization_data?client_version=15"
headers = {"Cookie": f"sessionid={session_cookie}"}
response = requests.get(url, headers=headers)
if response.status_code == 200:
try:
data = response.json()
globals_data = {item[0]: item[1] for item in data["globals"]}
storage = load_storage()
storage["userid"] = globals_data["USER_ID"]
storage["joined"] = data["projectTreeData"]["mainProjectTreeInfo"][
"dateJoinedTimestampInSeconds"
]
storage["transid"] = data["projectTreeData"]["mainProjectTreeInfo"][
"initialMostRecentOperationTransactionId"
]
storage["pollid"] = generate_uuid() # Simulate g() + g()
storage["root"] = {"nm": "root", "ch": data["projectTreeData"]["mainProjectTreeInfo"]["rootProjectChildren"]}
save_storage(storage)
console.log("Successfully refreshed and saved Workflowy data.")
return True
except Exception as e:
console.log(f"Error parsing response: {e}")
return False
else:
console.log(f"Error fetching Workflowy data: Status code {response.status_code}")
return False
# }}}
def check_workflowy_data(): # {{{
storage = load_storage()
if not storage:
console.log("Workflowy data is not initialized. Run the initialization first.")
return False
if not storage.get("userid") or not storage.get("transid") or not storage.get(
"pollid"
):
console.log("Workflowy data is incomplete. Run the initialization again.")
return False
return True
# }}}
def clip_to_workflowy(name, description, parent_id): # {{{
storage = load_storage()
if not storage:
console.log("Workflowy data is not initialized. Run the initialization first.")
return
new_uuid = generate_uuid()
timestamp = int(time.time()) - storage.get("joined", 0)
request = [
{
"most_recent_operation_transaction_id": storage.get("transid"),
"operations": [
{
"type": "create",
"data": {
"projectid": new_uuid,
"parentid": parent_id,
"priority": 9999,
},
"client_timestamp": timestamp,
"undo_data": {},
},
{
"type": "edit",
"data": {
"projectid": new_uuid,
"name": name,
"description": description,
},
"client_timestamp": timestamp,
"undo_data": {
"previous_last_modified": timestamp,
"previous_name": "",
"previous_description": "",
},
},
],
}
]
data = {
"client_id": "2015-11-17 19:25:15.397732",
"client_version": 15,
"push_poll_id": storage.get("pollid"),
"push_poll_data": json.dumps(request),
"crosscheck_user_id": storage.get("userid"),
}
headers = {"Cookie": f"sessionid={storage.get('session_cookie')}"}
response = requests.post("https://workflowy.com/push_and_poll", data=data, headers=headers)
if response.status_code == 200:
resp_obj = response.json()
if resp_obj.get("logged_out"):
console.log("Error: Logged out of Workflowy!")
elif not resp_obj.get("results"):
console.log("Error: Unknown error!")
else:
storage["transid"] = resp_obj["results"][0][
"new_most_recent_operation_transaction_id"
]
save_storage(storage)
console.print("[green]Successfully clipped to Workflowy![/green]")
else:
console.log(f"Error: Failed with status code {response.status_code}")
# }}}
def simplify_project(project_data, full_data, follow_mirrors=False): # {{{
if project_data.get("metadata", {}).get("mirror", {}).get("originalId"):
if follow_mirrors:
originalId = project_data["metadata"]["mirror"]["originalId"]
project_data, full_data = find_project_by_id(full_data, full_data, originalId)
else:
return None, full_data
if project_data.get("metadata", {}).get("isReferencesRoot"):
if project_data.get("ch"):
if len(project_data["ch"]) > 0:
project_data["nm"] = "backlinks"
project_data["metadata"]["layoutMode"] = "h1"
else:
return None, full_data
else:
return None, full_data
if project_data:
simplified_project = {
"name": project_data.get("nm", ""),
"id": project_data.get("id", ""),
"children": [],
"description": project_data.get("no", "").rstrip().replace("\n$", ""),
"format": project_data.get("metadata", {}).get("layoutMode", None)
}
children = project_data.get("ch", [])
for child in children:
simplified_child, full_data = simplify_project(child, full_data, follow_mirrors=follow_mirrors)
if simplified_child:
simplified_project["children"].append(simplified_child)
return simplified_project, full_data
return None, full_data
# }}}
def flatten(children): # {{{
flattened = []
for child in children:
try:
grand_children = child.get("children", [])
except Exception as e:
print(f"child: {child} {e}")
grand_children = []
child["children"] = []
flattened.append(child)
flattened.extend(flatten(grand_children))
return flattened
# }}}
def flatten_project(project_data): # {{{
project_data["children"] = flatten(project_data.get("children", []))
return project_data
# }}}
# def filter_project_any(project_data, filters): # {{{
# filtered_project = {
# "name": project_data["name"],
# "id": project_data.get("id", ""),
# # "description": project_data["description"],
# "children": [],
# "format": project_data["format"]
# }
# children = project_data.get("children", [])
# for child in children:
# include = False
# for filter_text in filters:
# if filter_text.lower() in child["name"].lower():
# include = True
# break
# if include:
# filtered_project["children"].append(filter_project_any(child, filters))
# return filtered_project
def filter_project_any(project_data, filters, include_headers=False, all_children=False): # {{{
include = False
if include_headers:
if project_data["format"] == "h1" or project_data["format"] == "h2":
include = True
for filter_text in filters:
if filter_text in project_data["name"]:
include = True
break
children = []
for child in project_data.get("children", []):
if all_children and include:
logger.debug(f"Not filtering children of {project_data['name']}")
pass
else:
child = filter_project_any(child, filters, include_headers=include_headers, all_children=all_children)
if child:
children.append(child)
project_data["children"] = children
if include or children:
return project_data
else:
return None
# }}}
def filter_project_all(project_data, filters, include_headers=False, all_children=False): # {{{
include = True
if include_headers and (project_data["format"] == "h1" or project_data["format"] == "h2"):
pass
else:
for filter_text in filters:
if filter_text not in project_data["name"]:
if filter_text not in project_data.get("description", ""):
include = False
break
if include:
logger.debug(f"Including {project_data['name']}")
logger.debug(f"all_children: {all_children}")
children = []
for child in project_data.get("children", []):
if all_children and include:
logger.debug(f"Not filtering children of {project_data['name']}")
pass
else:
child = filter_project_all(child, filters, include_headers=include_headers, all_children=all_children)
if child:
children.append(child)
project_data["children"] = children
if include or children:
return project_data
else:
return None
# }}}
def replace(project_data, regex, replacement): # {{{
project_data["name"] = re.sub(regex, replacement, project_data["name"])
children = project_data.get("children", [])
for child in children:
replace(child, regex, replacement)
return project_data
# }}}
def strip(project_data, regex): # {{{
project_data = replace(project_data, regex, "")
return project_data
# }}}
def remove_double_spaces(project_data): # {{{
# project_data["name"] = re.sub(r"\s+", " ", project_data["name"])
project_data["name"] = project_data["name"].replace(" ", " ")
children = project_data.get("children", [])
for child in children:
remove_double_spaces(child)
return project_data
# }}}
highlights = {
"@done": "green",
"@missed": "red",
"@na": "blue",
"#WORK": "red",
}
def highlight(project_data): # {{{
for key, value in highlights.items():
regex = f"{key}\\b"
project_data["name"] = re.sub(regex, f"[{value}]{key}[/]", project_data["name"])
children = project_data.get("children", [])
for child in children:
highlight(child)
return project_data
# }}}
colors1 = [
[["xTASK", "#READY"], "blue", True],
[["xTASK", "#MAYBE"], "violet", True],
[["xTASK", "#WAITING"], "cyan", True],
[["xTASK", "#DAILY"], "green", True],
[["xTASK", "#WEEKLY"], "green", True],
[["xTASK", "#IRREGULAR"], "green", True],
[["xPROJECT", "#ACTIVE"], "magenta", True],
[["xPROJECT", "#STALLED"], "cyan", True],
[["xPROJECT", "#PLANT"], "orange", True],
[["xSOMEDAY"], "violet", True],
[["xHABIT"], "orange", True],
[["xSTORY"], "cyan", True],
[["xGOAL"], "yellow", True],
[["xVIS"], "red", True],
[["xRESPONSIBILITY"], "red", True],
[["Sunday"], "underline violet", False],
[["Monday"], "underline red", False],
[["Tuesday"], "underline cyan", False],
[["Wednesday"], "underline magenta", False],
[["Thursday"], "underline green", False],
[["Friday"], "underline yellow", False],
[["Saturday"], "underline blue", False],
[["#r"], "black on blue", False],
[["#g"], "black on yellow", False],
[["#w"], "red", False],
[["#p"], "green", False],
]
def recolor(project_data, colors): # {{{
for rule in colors:
keywords = rule[0]
color = rule[1]
hide = rule[2]
match = True
for keyword in keywords:
if keyword not in project_data["name"]:
match = False
break
if match:
project_data["name"] = f"[{color}]{project_data['name']}[/]"
if hide:
for keyword in keywords:
project_data["name"] = project_data["name"].replace(keyword, "")
project_data["name"] = project_data["name"].rstrip() + f" [base01]{" ".join(keywords)}[/]"
children = project_data.get("children", [])
for child in children:
recolor(child, colors)
return project_data
# }}}
def print_pretty(data, indent=0, color="grey", show_description=True, show_id=False): # {{{
try:
for item in data["children"]:
if item["name"] == "backlinks":
# console.print(" " * indent + "[base01]• backlinks[/]")
continue
console.print(" " * indent + f"[base3]•[/] [{color}]{item['name']}[/][base01]{' ' + item['id'].split('-')[4] if show_id else ''}[/]")
if item["description"] and show_description:
console.print(" " * (indent + 1) + f"[base01]{item['description'].replace('\n', '\n' + ' ' * (indent + 1))}[/]")
if item["children"]:
print_pretty(item, indent + 1, color, show_description=show_description, show_id=show_id)
except Exception as e:
console.log(f"Error: {e}")
# }}}
def generate_d3_mindmap_html(data, show_description=True, show_id=False): # {{{
import json
import re
# Map of Rich tag colors to hex values or CSS color names
color_map = {
"base3": "#073642",
"base2": "#002b36",
"base1": "#586e75",
"base0": "#657b83",
"base00": "#839496",
"base01": "#93a1a1",
"base02": "#eee8d5",
"base03": "#fdf6e3",
"yellow": "#b58900",
"orange": "#cb4b16",
"red": "#dc322f",
"magenta": "#d33682",
"violet": "#6c71c4",
"blue": "#268bd2",
"cyan": "#2aa198",
"green": "#859900",
}
def parse_rich_tags(text):
text = re.sub(r"\[(underline)? ?([a-zA-Z0-9]+)?\](.*?)\[/\]",
lambda m: f'