├── .gitignore ├── requirements.txt ├── icon.png ├── README.md ├── lib ├── preprocess.py ├── create-prediction.py └── date.py └── info.plist /.gitignore: -------------------------------------------------------------------------------- 1 | /prefs.plist 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dateparser 2 | requests 3 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/alfred-fatebook-workflow/main/icon.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alfred-fatebook-workflow 2 | [Fatebook.io](https://fatebook.io/) workflow for [Alfred](https://www.alfredapp.com/). 3 | 4 | Type forecast to register a new Fatebook prediction. 5 | 6 | Make sure to configure the workflow by [adding your api key](https://fatebook.io/api-setup), then you can just type forecast, and it should be pretty straightforward. 7 | 8 | * v 1.1 - added default time of one week after today 9 | * v 1.2 - better date input and added success notification 10 | * v 2.0 - Support for guessing date from original text, extracting tags from original text, natural language date parsing, and ability to set a default team to share with. 11 | 12 | ## Development 13 | 14 | Installing dependencies: 15 | ```bash 16 | pip3 install -r requirements.txt --target ./lib 17 | ``` 18 | -------------------------------------------------------------------------------- /lib/preprocess.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dateparser import search 3 | import json 4 | import re 5 | 6 | def extract_date(input_text): 7 | """ 8 | Extracts the first future date from the input text using dateparser. 9 | Returns the ISO formatted date as a string, or None if no date is found. 10 | """ 11 | date_suggestions = search.search_dates(input_text, settings={'PREFER_DATES_FROM': 'future'}) 12 | if date_suggestions and date_suggestions[0]: 13 | return date_suggestions[0][1].strftime('%Y-%m-%d') 14 | return None 15 | 16 | def extract_tags(input_text): 17 | """ 18 | Extracts hashtags (#tag) and multi-word tags ([[multi word tag]]) from the input text. 19 | Returns a list of tags with hashtags stripped of the '#' symbol. 20 | """ 21 | # Define patterns 22 | hashtag_pattern = r"#\w+" 23 | multi_word_tag_pattern = r"\[\[.*?\]\]" 24 | 25 | # Extract single-word hashtags and strip '#' 26 | hashtags = [tag[1:] for tag in re.findall(hashtag_pattern, input_text)] # Remove '#' from tags 27 | 28 | # Extract multi-word tags and remove brackets 29 | multi_word_tags = [tag.strip("[[]]") for tag in re.findall(multi_word_tag_pattern, input_text)] 30 | 31 | # Combine both types of tags 32 | return hashtags + multi_word_tags 33 | 34 | def build_output(iso_date, tags): 35 | """ 36 | Constructs the Alfred workflow JSON output with the given date and tags. 37 | Returns the output as a dictionary. 38 | """ 39 | return { 40 | "alfredworkflow": { 41 | "arg": iso_date or "", 42 | "variables": { 43 | "tags": tags 44 | } 45 | } 46 | } 47 | 48 | def main(): 49 | input_text = sys.argv[1] 50 | 51 | # Extract the date and tags 52 | iso_date = extract_date(input_text) 53 | tags = extract_tags(input_text) 54 | 55 | # Build the output 56 | alfred_output = build_output(iso_date, tags) 57 | 58 | # Print the result as JSON 59 | print(json.dumps(alfred_output)) 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /lib/create-prediction.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | 5 | 6 | def main(): 7 | # Read environment variables 8 | tags = os.getenv("tags", "") # Tags from the environment variable 9 | question = os.getenv("question") # Question from the environment variable 10 | user_date = os.getenv("user_date") # Date from the environment variable 11 | percentage = float(os.getenv("percentage")) # Percentage from the environment variable 12 | fatebook_api_key = os.getenv("fatebook_api_key") # API key from the environment variable 13 | share_with_team = os.getenv("shareWithTeam") # Team name from the environment variable (optional) 14 | 15 | # Ensure required environment variables are set 16 | if not all([question, user_date, fatebook_api_key]): 17 | raise ValueError( 18 | "Missing one or more required environment variables: 'question', 'user_date', or 'fatebook_api_key'." 19 | ) 20 | 21 | # Convert percentage to decimal format 22 | forecast_percentage = round(percentage / 100, 2) 23 | 24 | # Parse tags if present (tab-separated) 25 | tags_list = tags.split("\t") if tags else [] 26 | 27 | # Construct the query parameters 28 | query_params = { 29 | "apiKey": fatebook_api_key, 30 | "title": question, 31 | "resolveBy": user_date, 32 | "forecast": forecast_percentage, 33 | } 34 | 35 | # Add tags to the query parameters as repeated keys 36 | query_params_list = [] 37 | for key, value in query_params.items(): 38 | query_params_list.append((key, value)) 39 | for tag in tags_list: 40 | query_params_list.append(("tags", tag.strip())) 41 | 42 | # Add shareWithLists parameter if shareWithTeam is set 43 | if share_with_team: 44 | query_params_list.append(("shareWithLists", share_with_team)) 45 | 46 | # Make the PUT request 47 | base_url = "https://fatebook.io/api/v0/createQuestion" 48 | response = requests.put(base_url, params=query_params_list) 49 | 50 | # Output the result in Alfred workflow format 51 | print(json.dumps({ 52 | "alfredworkflow": { 53 | "arg": response.text, 54 | } 55 | })) 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | -------------------------------------------------------------------------------- /lib/date.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | from dateparser import parse 4 | from datetime import datetime, timedelta 5 | 6 | def eprint(*args, **kwargs): 7 | print(*args, file=sys.stderr, **kwargs) 8 | 9 | def get_common_dates(): 10 | now = datetime.now() 11 | common_dates = { 12 | "Today": now.strftime('%Y-%m-%d'), 13 | "Tomorrow": (now + timedelta(days=1)).strftime('%Y-%m-%d'), 14 | "Day After Tomorrow": (now + timedelta(days=2)).strftime('%Y-%m-%d'), 15 | "End of This Week": (now + timedelta(days=(6 - now.weekday()))).strftime('%Y-%m-%d'), 16 | "Start of Next Week": (now + timedelta(days=(7 - now.weekday()))).strftime('%Y-%m-%d'), 17 | "End of This Month": (now.replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1), 18 | "Start of Next Month": (now.replace(day=28) + timedelta(days=4)).replace(day=1), 19 | "End of This Year": datetime(now.year, 12, 31).strftime('%Y-%m-%d'), 20 | "Start of Next Year": datetime(now.year + 1, 1, 1).strftime('%Y-%m-%d'), 21 | } 22 | return {k: v.strftime('%Y-%m-%d') if isinstance(v, datetime) else v for k, v in common_dates.items()} 23 | 24 | def get_iso_date(user_input): 25 | try: 26 | date_obj = parse(user_input, settings={'PREFER_DATES_FROM': 'future'}) 27 | if date_obj: 28 | iso_date = date_obj.strftime('%Y-%m-%d') 29 | return iso_date 30 | return None 31 | except Exception as e: 32 | eprint(f"Error parsing date: {e}") 33 | return None 34 | 35 | def main(): 36 | user_input = sys.argv[1] if len(sys.argv) > 1 else None 37 | items = [] 38 | 39 | if user_input: 40 | iso_date = get_iso_date(user_input) 41 | if iso_date: 42 | items.append({ 43 | "title": iso_date, 44 | "arg": iso_date 45 | }) 46 | else: 47 | items.append({ 48 | "title": f"Could not parse date: {user_input}", 49 | "arg": "" 50 | }) 51 | else: 52 | # Show common dates if there is no user input 53 | common_dates = get_common_dates() 54 | for label, date in common_dates.items(): 55 | items.append({ 56 | "title": f"{label}: {date}", 57 | "arg": date 58 | }) 59 | 60 | print(json.dumps({"items": items})) 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | 7 | connections 8 | 9 | 07AB99AE-90CD-4AB2-8E3B-4A5B61C323FD 10 | 11 | 12 | destinationuid 13 | 60F85D66-6768-47CC-BCA3-CC5F41B2EE67 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | vitoclose 19 | 20 | 21 | 22 | 3DC2B61F-8581-463A-929E-D5FF4CADA315 23 | 24 | 25 | destinationuid 26 | 975F47B3-CCA8-4619-B1EA-C3F553DC28D5 27 | modifiers 28 | 0 29 | modifiersubtext 30 | 31 | vitoclose 32 | 33 | 34 | 35 | 4AB639F3-D097-4057-B74F-0AF68F657B06 36 | 37 | 38 | destinationuid 39 | FA01F828-9AF9-4D92-9394-C318C66579E4 40 | modifiers 41 | 0 42 | modifiersubtext 43 | 44 | vitoclose 45 | 46 | 47 | 48 | 60F85D66-6768-47CC-BCA3-CC5F41B2EE67 49 | 50 | 51 | destinationuid 52 | 3DC2B61F-8581-463A-929E-D5FF4CADA315 53 | modifiers 54 | 0 55 | modifiersubtext 56 | 57 | vitoclose 58 | 59 | 60 | 61 | 6E4EEBD5-0AB8-4986-8EC0-1CD707F41689 62 | 63 | 64 | destinationuid 65 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 66 | modifiers 67 | 0 68 | modifiersubtext 69 | 70 | vitoclose 71 | 72 | 73 | 74 | 7EDD178C-04AD-4CCD-BE42-593CF9B98EFE 75 | 76 | 77 | destinationuid 78 | CADA36B1-B2FE-4217-AC88-B3EECD0C5C21 79 | modifiers 80 | 0 81 | modifiersubtext 82 | 83 | vitoclose 84 | 85 | 86 | 87 | 975F47B3-CCA8-4619-B1EA-C3F553DC28D5 88 | 89 | 90 | destinationuid 91 | 4AB639F3-D097-4057-B74F-0AF68F657B06 92 | modifiers 93 | 0 94 | modifiersubtext 95 | 96 | vitoclose 97 | 98 | 99 | 100 | 9A5E66D7-F206-4686-AB82-CC9ABC532414 101 | 102 | 103 | destinationuid 104 | 6E4EEBD5-0AB8-4986-8EC0-1CD707F41689 105 | modifiers 106 | 0 107 | modifiersubtext 108 | 109 | vitoclose 110 | 111 | 112 | 113 | A5B5C8BC-F2CF-40F4-9E6E-D127CF711EF5 114 | 115 | 116 | destinationuid 117 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 118 | modifiers 119 | 0 120 | modifiersubtext 121 | 122 | vitoclose 123 | 124 | 125 | 126 | CADA36B1-B2FE-4217-AC88-B3EECD0C5C21 127 | 128 | 129 | destinationuid 130 | D9A103A6-7AC1-421A-AD74-FE96710971A8 131 | modifiers 132 | 0 133 | modifiersubtext 134 | 135 | vitoclose 136 | 137 | 138 | 139 | destinationuid 140 | 67C7893A-5E82-4E05-97B1-6EF475BD94FD 141 | modifiers 142 | 0 143 | modifiersubtext 144 | 145 | vitoclose 146 | 147 | 148 | 149 | D330D9B5-F17F-4B6D-8B4A-BE45D0EBB227 150 | 151 | 152 | destinationuid 153 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 154 | modifiers 155 | 0 156 | modifiersubtext 157 | 158 | vitoclose 159 | 160 | 161 | 162 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 163 | 164 | 165 | destinationuid 166 | 07AB99AE-90CD-4AB2-8E3B-4A5B61C323FD 167 | modifiers 168 | 0 169 | modifiersubtext 170 | 171 | vitoclose 172 | 173 | 174 | 175 | FA01F828-9AF9-4D92-9394-C318C66579E4 176 | 177 | 178 | destinationuid 179 | 7EDD178C-04AD-4CCD-BE42-593CF9B98EFE 180 | modifiers 181 | 0 182 | modifiersubtext 183 | 184 | vitoclose 185 | 186 | 187 | 188 | 189 | createdby 190 | Caleb Parikh, Vlad Sitalo 191 | description 192 | Registers a fatebook prediction 193 | disabled 194 | 195 | name 196 | Fatebook 197 | objects 198 | 199 | 200 | config 201 | 202 | argumenttype 203 | 0 204 | keyword 205 | forecast 206 | subtext 207 | 208 | text 209 | Enter a question 210 | withspace 211 | 212 | 213 | type 214 | alfred.workflow.input.keyword 215 | uid 216 | 6E4EEBD5-0AB8-4986-8EC0-1CD707F41689 217 | version 218 | 1 219 | 220 | 221 | config 222 | 223 | alfredfiltersresults 224 | 225 | alfredfiltersresultsmatchmode 226 | 0 227 | argumenttreatemptyqueryasnil 228 | 229 | argumenttrimmode 230 | 0 231 | argumenttype 232 | 1 233 | escaping 234 | 68 235 | keyword 236 | Date 237 | queuedelaycustom 238 | 3 239 | queuedelayimmediatelyinitially 240 | 241 | queuedelaymode 242 | 0 243 | queuemode 244 | 1 245 | runningsubtext 246 | 247 | script 248 | python3 ./lib/date.py "{query}" 249 | >&2 echo "${tags}" 250 | scriptargtype 251 | 0 252 | scriptfile 253 | 254 | subtext 255 | Options : today, tomorrow, next week .... or give a custom date as YYYY-MM-DD 256 | title 257 | When does this resolve by (YYYY-MM-DD)? Defaults to tomorrow. 258 | type 259 | 0 260 | withspace 261 | 262 | 263 | type 264 | alfred.workflow.input.scriptfilter 265 | uid 266 | 60F85D66-6768-47CC-BCA3-CC5F41B2EE67 267 | version 268 | 3 269 | 270 | 271 | config 272 | 273 | concurrently 274 | 275 | escaping 276 | 102 277 | script 278 | python3 ./lib/preprocess.py "${question}" 279 | scriptargtype 280 | 1 281 | scriptfile 282 | 283 | type 284 | 0 285 | 286 | type 287 | alfred.workflow.action.script 288 | uid 289 | 07AB99AE-90CD-4AB2-8E3B-4A5B61C323FD 290 | version 291 | 2 292 | 293 | 294 | config 295 | 296 | argument 297 | 298 | passthroughargument 299 | 300 | variables 301 | 302 | question 303 | {query} 304 | 305 | 306 | type 307 | alfred.workflow.utility.argument 308 | uid 309 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 310 | version 311 | 1 312 | 313 | 314 | config 315 | 316 | argumenttype 317 | 0 318 | keyword 319 | predict 320 | subtext 321 | 322 | text 323 | Enter a question 324 | withspace 325 | 326 | 327 | type 328 | alfred.workflow.input.keyword 329 | uid 330 | A5B5C8BC-F2CF-40F4-9E6E-D127CF711EF5 331 | version 332 | 1 333 | 334 | 335 | config 336 | 337 | matchmode 338 | 2 339 | matchstring 340 | 341 | replacestring 342 | {isodate +1D:yyyy-MM-dd} 343 | 344 | type 345 | alfred.workflow.utility.replace 346 | uid 347 | 3DC2B61F-8581-463A-929E-D5FF4CADA315 348 | version 349 | 2 350 | 351 | 352 | config 353 | 354 | argument 355 | 356 | passthroughargument 357 | 358 | variables 359 | 360 | user_date 361 | {query} 362 | 363 | 364 | type 365 | alfred.workflow.utility.argument 366 | uid 367 | 975F47B3-CCA8-4619-B1EA-C3F553DC28D5 368 | version 369 | 1 370 | 371 | 372 | config 373 | 374 | argumenttype 375 | 0 376 | keyword 377 | fatebook 378 | subtext 379 | 380 | text 381 | Enter a question 382 | withspace 383 | 384 | 385 | type 386 | alfred.workflow.input.keyword 387 | uid 388 | D330D9B5-F17F-4B6D-8B4A-BE45D0EBB227 389 | version 390 | 1 391 | 392 | 393 | config 394 | 395 | autopaste 396 | 397 | clipboardtext 398 | {query} 399 | ignoredynamicplaceholders 400 | 401 | transient 402 | 403 | 404 | type 405 | alfred.workflow.output.clipboard 406 | uid 407 | D9A103A6-7AC1-421A-AD74-FE96710971A8 408 | version 409 | 3 410 | 411 | 412 | config 413 | 414 | argumenttype 415 | 0 416 | keyword 417 | percentage 418 | subtext 419 | 40 420 | text 421 | Please enter a probability (%) between 1 and 100 422 | withspace 423 | 424 | 425 | type 426 | alfred.workflow.input.keyword 427 | uid 428 | 4AB639F3-D097-4057-B74F-0AF68F657B06 429 | version 430 | 1 431 | 432 | 433 | config 434 | 435 | concurrently 436 | 437 | escaping 438 | 102 439 | script 440 | python3 ./lib/create-prediction.py "{query}" 441 | scriptargtype 442 | 1 443 | scriptfile 444 | 445 | type 446 | 0 447 | 448 | type 449 | alfred.workflow.action.script 450 | uid 451 | 7EDD178C-04AD-4CCD-BE42-593CF9B98EFE 452 | version 453 | 2 454 | 455 | 456 | config 457 | 458 | argument 459 | 460 | passthroughargument 461 | 462 | variables 463 | 464 | percentage 465 | {query} 466 | 467 | 468 | type 469 | alfred.workflow.utility.argument 470 | uid 471 | FA01F828-9AF9-4D92-9394-C318C66579E4 472 | version 473 | 1 474 | 475 | 476 | config 477 | 478 | action 479 | 0 480 | argument 481 | 0 482 | focusedappvariable 483 | 484 | focusedappvariablename 485 | 486 | hotkey 487 | 14 488 | hotmod 489 | 1179648 490 | hotstring 491 | F 492 | leftcursor 493 | 494 | modsmode 495 | 0 496 | relatedAppsMode 497 | 0 498 | 499 | type 500 | alfred.workflow.trigger.hotkey 501 | uid 502 | 9A5E66D7-F206-4686-AB82-CC9ABC532414 503 | version 504 | 2 505 | 506 | 507 | config 508 | 509 | argument 510 | '{query}', {variables} 511 | cleardebuggertext 512 | 513 | processoutputs 514 | 515 | 516 | type 517 | alfred.workflow.utility.debug 518 | uid 519 | CADA36B1-B2FE-4217-AC88-B3EECD0C5C21 520 | version 521 | 1 522 | 523 | 524 | config 525 | 526 | lastpathcomponent 527 | 528 | onlyshowifquerypopulated 529 | 530 | removeextension 531 | 532 | text 533 | "{var:question}" ({var:percentage}) resolves on {var:user_date} 534 | title 535 | Added forecast! 536 | 537 | type 538 | alfred.workflow.output.notification 539 | uid 540 | 67C7893A-5E82-4E05-97B1-6EF475BD94FD 541 | version 542 | 1 543 | 544 | 545 | readme 546 | Type forecast to register a new Fatebook prediction. 547 | 548 | Make sure to configure the workflow by adding your api key, then you can just type forecast and it should be pretty straightforward. 549 | 550 | v 1.1 - added default time of one week after today 551 | v 1.2 - better date input and added success notification 552 | v 1.4 - natural language date parsing, copy URL to clipboard 553 | v 2.0 - Support for guessing date from original text, extracting tags from original text, natural language date parsin, and ability to set a default team to share with. 554 | uidata 555 | 556 | 07AB99AE-90CD-4AB2-8E3B-4A5B61C323FD 557 | 558 | xpos 559 | 420 560 | ypos 561 | 50 562 | 563 | 3DC2B61F-8581-463A-929E-D5FF4CADA315 564 | 565 | xpos 566 | 865 567 | ypos 568 | 190 569 | 570 | 4AB639F3-D097-4057-B74F-0AF68F657B06 571 | 572 | xpos 573 | 290 574 | ypos 575 | 395 576 | 577 | 60F85D66-6768-47CC-BCA3-CC5F41B2EE67 578 | 579 | xpos 580 | 650 581 | ypos 582 | 35 583 | 584 | 67C7893A-5E82-4E05-97B1-6EF475BD94FD 585 | 586 | xpos 587 | 675 588 | ypos 589 | 585 590 | 591 | 6E4EEBD5-0AB8-4986-8EC0-1CD707F41689 592 | 593 | xpos 594 | 40 595 | ypos 596 | 30 597 | 598 | 7EDD178C-04AD-4CCD-BE42-593CF9B98EFE 599 | 600 | xpos 601 | 555 602 | ypos 603 | 400 604 | 605 | 975F47B3-CCA8-4619-B1EA-C3F553DC28D5 606 | 607 | xpos 608 | 465 609 | ypos 610 | 250 611 | 612 | 9A5E66D7-F206-4686-AB82-CC9ABC532414 613 | 614 | xpos 615 | 80 616 | ypos 617 | 435 618 | 619 | A5B5C8BC-F2CF-40F4-9E6E-D127CF711EF5 620 | 621 | xpos 622 | 40 623 | ypos 624 | 165 625 | 626 | CADA36B1-B2FE-4217-AC88-B3EECD0C5C21 627 | 628 | xpos 629 | 565 630 | ypos 631 | 555 632 | 633 | D330D9B5-F17F-4B6D-8B4A-BE45D0EBB227 634 | 635 | xpos 636 | 45 637 | ypos 638 | 290 639 | 640 | D9A103A6-7AC1-421A-AD74-FE96710971A8 641 | 642 | xpos 643 | 900 644 | ypos 645 | 340 646 | 647 | F9FCD21E-63FE-459B-83EB-A54FEFB623C1 648 | 649 | xpos 650 | 310 651 | ypos 652 | 115 653 | 654 | FA01F828-9AF9-4D92-9394-C318C66579E4 655 | 656 | xpos 657 | 465 658 | ypos 659 | 425 660 | 661 | 662 | userconfigurationconfig 663 | 664 | 665 | config 666 | 667 | default 668 | 669 | placeholder 670 | 671 | required 672 | 673 | trim 674 | 675 | 676 | description 677 | Get your api key from https://fatebook.io/api-setup 678 | label 679 | Fatebook Api Key 680 | type 681 | textfield 682 | variable 683 | fatebook_api_key 684 | 685 | 686 | config 687 | 688 | default 689 | 690 | placeholder 691 | 692 | required 693 | 694 | trim 695 | 696 | 697 | description 698 | The team to share your predictions with by default 699 | label 700 | Default Team 701 | type 702 | textfield 703 | variable 704 | shareWithTeam 705 | 706 | 707 | variablesdontexport 708 | 709 | version 710 | 2.0 711 | webaddress 712 | https://fatebook.io/ 713 | 714 | 715 | --------------------------------------------------------------------------------