Skip to content

Instantly share code, notes, and snippets.

@nornagon
Last active October 10, 2023 16:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nornagon/a89a654a20e41601b4f9 to your computer and use it in GitHub Desktop.
Save nornagon/a89a654a20e41601b4f9 to your computer and use it in GitHub Desktop.
My disaster of a piece of software for making Medium auto-publish drafts older than 60 minutes

Pile of hacks to work around the fact that Medium's API doesn't give access to drafts. Hijacks Chrome's cookies to pretend to be you.

Usage:

  1. Put disaster.py somewhere.
  2. Edit net.nornagon.writedamnyou.plist so the second string in ProgramArguments points to where you put disaster.py.
  3. Put the net.nornagon.writedamnyou.plist file in ~/Library/LaunchAgents
  4. Run launchctl load ~/Library/LaunchAgents/net.nornagon.writedamnyou.plist

Probably it'll ask you for access to your keychain, since that's needed to decrypt Chrome's cookie store.

#! /usr/bin/env python3
'''pyCookieCheat.py
2015022 Now its own GitHub repo, and in PyPi.
- For most recent version: https://github.com/n8henrie/pycookiecheat
- This gist unlikely to be maintained further for that reason.
20150221 v2.0.1: Now should find cookies for base domain and all subs.
20140518 v2.0: Now works with Chrome's new encrypted cookies.
See relevant post at http://n8h.me/HufI1w
Use your browser's cookies to make grabbing data from login-protected sites easier.
Intended for use with Python Requests http://python-requests.org
Accepts a URL from which it tries to extract a domain. If you want to horse the domain,
just send it the domain you'd like to use instead.
Intended use with requests:
import requests
import pyCookieCheat
url = 'http://www.example.com'
s = requests.Session()
cookies = pyCookieCheat.chrome_cookies(url)
s.get(url, cookies = cookies)
Adapted from my code at http://n8h.me/HufI1w
Helpful Links:
* Chromium Mac os_crypt: http://n8h.me/QWRgK8
* Chromium Linux os_crypt: http://n8h.me/QWTglz
* Python Crypto: http://n8h.me/QWTqte
'''
import sqlite3
import os.path
import urllib.parse
import keyring
import sys
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
def chrome_cookies(url):
salt = b'saltysalt'
iv = b' ' * 16
length = 16
def chrome_decrypt(encrypted_value, key=None):
# Encrypted cookies should be prefixed with 'v10' according to the
# Chromium code. Strip it off.
encrypted_value = encrypted_value[3:]
# Strip padding by taking off number indicated by padding
# eg if last is '\x0e' then ord('\x0e') == 14, so take off 14.
# You'll need to change this function to use ord() for python2.
def clean(x):
return x[:-x[-1]].decode('utf8')
cipher = AES.new(key, AES.MODE_CBC, IV=iv)
decrypted = cipher.decrypt(encrypted_value)
return clean(decrypted)
# If running Chrome on OSX
if sys.platform == 'darwin':
my_pass = keyring.get_password('Chrome Safe Storage', 'Chrome')
my_pass = my_pass.encode('utf8')
iterations = 1003
cookie_file = os.path.expanduser(
'~/Library/Application Support/Google/Chrome/Default/Cookies'
)
# If running Chromium on Linux
elif sys.platform == 'linux':
my_pass = 'peanuts'.encode('utf8')
iterations = 1
cookie_file = os.path.expanduser(
'~/.config/chromium/Default/Cookies'
)
else:
raise Exception("This script only works on OSX or Linux.")
# Generate key from values above
key = PBKDF2(my_pass, salt, length, iterations)
# Part of the domain name that will help the sqlite3 query pick it from the Chrome cookies
domain = urllib.parse.urlparse(url).netloc
domain_no_sub = '.'.join(domain.split('.')[-2:])
conn = sqlite3.connect(cookie_file)
sql = 'select name, value, encrypted_value from cookies '\
'where host_key like "%{}%"'.format(domain_no_sub)
cookies = {}
cookies_list = []
with conn:
for k, v, ev in conn.execute(sql):
# if there is a not encrypted value or if the encrypted value
# doesn't start with the 'v10' prefix, return v
if v or (ev[:3] != b'v10'):
cookies_list.append((k, v))
else:
decrypted_tuple = (k, chrome_decrypt(ev, key=key))
cookies_list.append(decrypted_tuple)
cookies.update(cookies_list)
return cookies
import requests, json, time
s = requests.Session()
url = "https://medium.com"
cookies = chrome_cookies(url)
s.headers.update({'accept': 'application/json'})
s.headers.update({'x-xsrf-token': cookies['xsrf']})
drafts_req = s.get("https://medium.com/me/stories/drafts", cookies = cookies)
drafts = json.loads(drafts_req.text[16:])
for p in drafts['payload']['posts']:
secondsAgo = time.time() - p['createdAt']/1000
print("%s (%s) created %d seconds ago" % (p['id'], p['title'], secondsAgo))
if secondsAgo >= 60*60 and secondsAgo <= 7*24*60*60:
res = s.post("https://medium.com/p/%s/publish" % p['id'], data = {'title': '', 'subtitle': '', 'latestRev': p['latestRev']}, cookies = cookies)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.nornagon.writedamnyou</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/python3</string>
<string>/Users/nornagon/Source/medium-hax/disaster.py</string>
</array>
<key>StartInterval</key>
<integer>600</integer>
</dict>
</plist>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment