├── apiserver ├── README.md └── server.py ├── scripts ├── fetch-firmware.sh ├── latest.py └── unpack.py ├── README.md ├── bots └── Discord_Archive └── LICENSE /apiserver/README.md: -------------------------------------------------------------------------------- 1 | # Goal 2 | 3 | The goal of this effort is to replace `api.cricut.com` with a local 4 | server, entirely detached from the upstream server. This can provide 5 | functionality when a user is not connected to the internet, or simply 6 | when a user chooses to keep all data local/private. 7 | 8 | There are three main points to using this server: 9 | 10 | * modify `/etc/hosts` to point `api.cricut.com` to localhost 11 | (or the Windows equivalent? help?), and run the server locally 12 | * modify the DNS server used by the client/desktop to point 13 | to an instance of this server 14 | * configuring/running this server to manage requests 15 | -------------------------------------------------------------------------------- /scripts/fetch-firmware.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Download ALL known firmware binaries to the current directory 4 | # 5 | # ------ 6 | # For codenames and versions, see: 7 | # https://github.com/CutFreedom/grasshopper/wiki/Firmware 8 | # or 9 | # https://help.cricut.com/hc/en-us/articles/360009504953-How-do-I-find-the-current-firmware-version-on-my-machine- 10 | # 11 | 12 | BASE=https://imgservice.cricut.com/design-public-mirror1/software/Firmware 13 | 14 | curl --remote-name $BASE/Zorro/FirmwareZorro-1.091.bin 15 | curl --remote-name $BASE/Helium/FirmwareHelium-3.091.bin 16 | curl --remote-name $BASE/Helium2/FirmwareHelium2-5.120.bin 17 | curl --remote-name $BASE/Warro/FirmwareWarro-2.098.bin 18 | curl --remote-name $BASE/Athena/FirmwareAthena-4.175.bin 19 | 20 | # Download some non-current, if they are still available 21 | curl --remote-name $BASE/Warro/FirmwareWarro-2.095.bin 22 | -------------------------------------------------------------------------------- /scripts/latest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple script to fetch "latest.json" files for information about 4 | # the latest platform executables. 5 | # 6 | 7 | import time 8 | import json 9 | import http.client 10 | 11 | 12 | DESIGN_HOST = 'staticcontent.cricut.com' 13 | DESIGN_ROOT = '/a/software' 14 | 15 | PLATFORMS = { 16 | 'osx-native', 17 | 'win32-native', 18 | } 19 | 20 | # 'osx-plugin', 21 | 22 | 23 | def fetch_latest(platform): 24 | conn = http.client.HTTPSConnection(DESIGN_HOST) 25 | path = f'{DESIGN_ROOT}/{platform}/latest.json' 26 | print('PATH:', path) 27 | conn.request('GET', path) 28 | r = conn.getresponse() 29 | content = r.read() 30 | return json.loads(content) 31 | 32 | 33 | def latest_info(): 34 | for p in PLATFORMS: 35 | data = fetch_latest(p) 36 | #print('DATA:', data) 37 | print('PLATFORM:', p) 38 | print(' rollout start:', time.ctime(data['rolloutStart'])) 39 | print(' end:', time.ctime(data['rolloutEnd'])) 40 | print(f' base: {data["baseVersion"]} via "{DESIGN_ROOT}/{p}/{data["baseInstallFile"]}"') 41 | print(f' rollout: {data["rolloutVersion"]} via "{DESIGN_ROOT}/{p}/{data["rolloutInstallFile"]}"') 42 | # also: paused, rolloutUpdateFile, rolloutUpdateFileHash, enforcedFeatureConfig 43 | 44 | if __name__ == '__main__': 45 | latest_info() 46 | -------------------------------------------------------------------------------- /scripts/unpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Simple script to extract the "app.asar" Electron application. 4 | # 5 | # USAGE: 6 | # $ ./unpack.py /path/to/mounted/dmg outputAppDir 7 | # 8 | 9 | import sys 10 | import os.path 11 | import struct 12 | import json 13 | 14 | import asar 15 | 16 | ASAR_PATH = 'Contents/Resources/app.asar' 17 | 18 | 19 | def open_archive(fname): 20 | "Open an ASAR archive." 21 | 22 | # FORMAT: 23 | # UINT32: size of HEADER_SIZE (always 4) 24 | # UINT32: size of HEADER field (including SIZE value) 25 | # UINT32: size of HEADER field 26 | # STRING: header 27 | # UINT32: length of HEADER 28 | # BYTES: header content 29 | # 30 | # NOTE: the HEADER STRING might be shorter than the HEADER FIELD. 31 | # Padding has been observed, which pads the content out to a 32 | # longer FIELD than the actual HEADER content. 33 | # 34 | fp = open(fname, 'rb') 35 | leader = fp.read(16) 36 | 37 | hdr_len = struct.unpack('/social') 103 | def profiles_social(user_id): 104 | pass 105 | 106 | @app.route('/profiles/v1/Profiles//follows/profiles/') 107 | def profiles_follows(user_id, follow): 108 | pass 109 | 110 | @app.route('/profiles/v1/Profiles/cricutId/') 111 | def profiles_cricut(id): 112 | pass 113 | 114 | #------------------------ 115 | 116 | @app.route('/projects/CanvasMigrationQueue') 117 | def projects_migration(): 118 | pass 119 | 120 | @app.route('/projects/Projects/v1/userProjects/') 121 | def projects_fetch(user_id): 122 | token = flask.request.args.get('token') 123 | size = flask.request.args.get('size') 124 | pub_only = flask.request.args.get('publishedOnly') 125 | 126 | @app.route('/projects/v1/Projects/search') 127 | def projects_search(): 128 | entitled = flask.request.args.get('entitledOnly') 129 | level = flask.request.args.get('projectDetailLevel') 130 | featured = flask.request.args.get('featured') 131 | size = flask.request.args.get('size') 132 | token = flask.request.args.get('token') 133 | family = flask.request.args.get('machineFamilyType') 134 | type = flask.request.args.get('type') 135 | translate = flask.request.args.get('translateQuery') 136 | 137 | @app.route('/projects/v1/projects/favorites/') # ?size=200 138 | def projects_favs(user_id): 139 | size = flask.request.args['size'] 140 | 141 | #------------------------ 142 | 143 | @app.route('/tags/v1/Categories') 144 | def tags(): 145 | is_community = flask.request.args.get('isCommunity') 146 | family = flask.request.args.get('machineFamilyType') 147 | size = flask.request.args.get('pageSize') 148 | type = flask.request.args.get('type') 149 | include_subcat = flask.request.args.get('includeSubCategories') 150 | 151 | #------------------------ 152 | 153 | @app.route('/v4/Entitlements/GetImageSetGroupExpirations') 154 | def v4_expirations(): 155 | pass 156 | 157 | @app.route('/v4/Lookups/GetAppSessionData') # appName=Gliese 158 | def v4_get_session(): 159 | app = flask.request.args['appName'] 160 | sid = uuid.uuid4() 161 | 162 | ### no idea what each element represents, but this is the order 163 | ### and the values that upstream delivers 164 | body = ( 165 | 'https://imgservice.cricut.com/design-public-mirror1/images/', 166 | 'https://imgservice.cricut.com/design-public-mirror1/templates/', 167 | '-api.cricut.com/v4/Images/GetUserImage?ImageID=', 168 | '', 169 | '', 170 | '100', 171 | '151', 172 | '', 173 | '1-1-1', 174 | 'http://mirror.cricut.com/project/', 175 | '1', 176 | '51766', 177 | KEPLER_J, 178 | 'https://imgservice.cricut.com/design-public-mirror1/software/', 179 | 'https://imgservice.cricut.com/design-public-mirror1/categories/', 180 | 'V4', 181 | 'https://s3-us-west-2.amazonaws.com/dev50-design-public/Fonts/', 182 | str(int(time.time())), 183 | ) 184 | ### maybe ensure the C-T includes the charset? 185 | response = flask.make_response(flask.jsonify(body)) 186 | response.set_cookie('SessionID-Prod', str(sid), 187 | expires=None, ### fix this 188 | path='/', 189 | domain=CRICUT_COM, 190 | ) 191 | ### fix these country values 192 | ### US=312, BE=334 193 | response.set_cookie('SelectedCountryID', '312', 194 | domain=CRICUT_COM, path='/') 195 | response.set_cookie('Country-Code', 'US', 196 | domain=CRICUT_COM, path='/') 197 | ### other response headers? 198 | return response 199 | 200 | KEPLER = { 201 | "Kepler": { 202 | "Windows": { 203 | "Available": { 204 | "Version": "3.2.1.0", 205 | "File": "CricutDesignSpace-3.2.1.0.exe", 206 | "Type":"Optional", 207 | }, 208 | "Required": { 209 | "Version": "3.2.1.0", 210 | "File": "CricutDesignSpace-3.2.1.0.exe", 211 | "Type": "Required", 212 | }, 213 | }, 214 | "MacOS": { 215 | "Available": { 216 | "Version": "3.2.1.0", 217 | "File": "CricutDesignSpace-3.2.1.0.zip", 218 | "Type": "Optional", 219 | }, 220 | "Required": { 221 | "Version": "3.2.1.0", 222 | "File": "CricutDesignSpace-3.2.1.0.zip", 223 | "Type": "Required", 224 | }, 225 | }, 226 | }, 227 | } 228 | KEPLER_J = json.dumps(KEPLER) 229 | 230 | @app.route('/v4/ShoppingCart/Quote') 231 | def v4_shopping_quote(): 232 | pass 233 | 234 | @app.route('/v4/Subscription/PurchasableSubscriptions') 235 | def v4_purchasable(): 236 | pass 237 | 238 | @app.route('/v4/Subscription/UserCricutAccessStatus') 239 | def v4_user_status(): 240 | pass 241 | 242 | @app.route('/v4/Users/GetLoggedInUser') 243 | def v4_get_user(): 244 | pass 245 | 246 | @app.route('/v4/Users/GetUserPreferencesAsync') 247 | def v4_get_prefs(): 248 | pass 249 | 250 | @app.route('/v4/Users/IsUserLoggedIn') 251 | def v4_is_loggedin(): 252 | pass 253 | 254 | @app.route('/v4/Users/Login') 255 | def v4_login(): 256 | pass 257 | 258 | @app.route('/v4/Users/SaveUserPreferencesAsync') 259 | def v4_save_prefs(): 260 | pass 261 | 262 | #------------------------ 263 | 264 | 265 | if __name__ == '__main__': 266 | # Run the daemon, listening at paths noted above. 267 | app.run(debug=True, host='0.0.0.0') 268 | --------------------------------------------------------------------------------