Codex commited on
Commit
7212c19
·
1 Parent(s): 63b4caa

Use Dropbox API for Circa file discovery

Browse files
.env.example CHANGED
@@ -8,6 +8,8 @@ ODDS_API_BASE_URL=https://api.the-odds-api.com/v4
8
  ODDS_API_SPORT_KEY=baseball_mlb
9
  ODDS_API_REGIONS=us
10
  ODDS_API_MARKETS=batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts
 
 
11
  CIRCA_DROPBOX_URL=
12
  SCAN_REPORT_CHANNEL_ID=
13
  SCAN_ALERT_CHANNEL_ID=
 
8
  ODDS_API_SPORT_KEY=baseball_mlb
9
  ODDS_API_REGIONS=us
10
  ODDS_API_MARKETS=batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts
11
+ DROPBOX_ACCESS_TOKEN=
12
+ CIRCA_DROPBOX_FOLDER_PATH=
13
  CIRCA_DROPBOX_URL=
14
  SCAN_REPORT_CHANNEL_ID=
15
  SCAN_ALERT_CHANNEL_ID=
README.md CHANGED
@@ -131,7 +131,9 @@ Environment variables for scanner features:
131
  - `ODDS_API_SPORT_KEY`
132
  - `ODDS_API_REGIONS`
133
  - `ODDS_API_MARKETS`
134
- - `CIRCA_DROPBOX_URL`
 
 
135
  - `SCAN_REPORT_CHANNEL_ID`
136
  - `SCAN_ALERT_CHANNEL_ID`
137
  - `SCAN_MORNING_TIME`
 
131
  - `ODDS_API_SPORT_KEY`
132
  - `ODDS_API_REGIONS`
133
  - `ODDS_API_MARKETS`
134
+ - `DROPBOX_ACCESS_TOKEN`
135
+ - `CIRCA_DROPBOX_FOLDER_PATH`
136
+ - `CIRCA_DROPBOX_URL` (deprecated fallback)
137
  - `SCAN_REPORT_CHANNEL_ID`
138
  - `SCAN_ALERT_CHANNEL_ID`
139
  - `SCAN_MORNING_TIME`
deploy/oracle/roibot.env.example CHANGED
@@ -8,6 +8,8 @@ ODDS_API_BASE_URL=https://api.the-odds-api.com/v4
8
  ODDS_API_SPORT_KEY=baseball_mlb
9
  ODDS_API_REGIONS=us
10
  ODDS_API_MARKETS=batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts
 
 
11
  CIRCA_DROPBOX_URL=
12
  SCAN_REPORT_CHANNEL_ID=
13
  SCAN_ALERT_CHANNEL_ID=
 
8
  ODDS_API_SPORT_KEY=baseball_mlb
9
  ODDS_API_REGIONS=us
10
  ODDS_API_MARKETS=batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts
11
+ DROPBOX_ACCESS_TOKEN=
12
+ CIRCA_DROPBOX_FOLDER_PATH=
13
  CIRCA_DROPBOX_URL=
14
  SCAN_REPORT_CHANNEL_ID=
15
  SCAN_ALERT_CHANNEL_ID=
package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "chart.js": "^4.5.0",
12
  "discord.js": "^14.25.1",
13
  "dotenv": "^17.2.3",
 
14
  "pdfjs-dist": "^5.6.205",
15
  "pg": "^8.16.3",
16
  "skia-canvas": "^3.0.8",
@@ -470,6 +471,17 @@
470
  "undici-types": "~7.18.0"
471
  }
472
  },
 
 
 
 
 
 
 
 
 
 
 
473
  "node_modules/@types/ws": {
474
  "version": "8.18.1",
475
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -498,6 +510,13 @@
498
  "node": ">= 14"
499
  }
500
  },
 
 
 
 
 
 
 
501
  "node_modules/bmp-js": {
502
  "version": "0.1.0",
503
  "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
@@ -527,7 +546,6 @@
527
  "version": "1.0.2",
528
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
529
  "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
530
- "dev": true,
531
  "license": "MIT",
532
  "dependencies": {
533
  "es-errors": "^1.3.0",
@@ -566,6 +584,19 @@
566
  "pnpm": ">=8"
567
  }
568
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  "node_modules/commander": {
570
  "version": "2.20.3",
571
  "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -608,6 +639,16 @@
608
  "url": "https://github.com/sponsors/ljharb"
609
  }
610
  },
 
 
 
 
 
 
 
 
 
 
611
  "node_modules/detect-libc": {
612
  "version": "2.1.2",
613
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -672,11 +713,25 @@
672
  "url": "https://dotenvx.com"
673
  }
674
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  "node_modules/dunder-proto": {
676
  "version": "1.0.1",
677
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
678
  "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
679
- "dev": true,
680
  "license": "MIT",
681
  "dependencies": {
682
  "call-bind-apply-helpers": "^1.0.1",
@@ -691,7 +746,6 @@
691
  "version": "1.0.1",
692
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
693
  "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
694
- "dev": true,
695
  "license": "MIT",
696
  "engines": {
697
  "node": ">= 0.4"
@@ -701,7 +755,6 @@
701
  "version": "1.3.0",
702
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
703
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
704
- "dev": true,
705
  "license": "MIT",
706
  "engines": {
707
  "node": ">= 0.4"
@@ -711,7 +764,6 @@
711
  "version": "1.1.1",
712
  "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
713
  "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
714
- "dev": true,
715
  "license": "MIT",
716
  "dependencies": {
717
  "es-errors": "^1.3.0"
@@ -720,6 +772,22 @@
720
  "node": ">= 0.4"
721
  }
722
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  "node_modules/fast-deep-equal": {
724
  "version": "3.1.3",
725
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -746,11 +814,27 @@
746
  }
747
  }
748
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  "node_modules/function-bind": {
750
  "version": "1.1.2",
751
  "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
752
  "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
753
- "dev": true,
754
  "license": "MIT",
755
  "funding": {
756
  "url": "https://github.com/sponsors/ljharb"
@@ -767,7 +851,6 @@
767
  "version": "1.3.0",
768
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
769
  "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
770
- "dev": true,
771
  "license": "MIT",
772
  "dependencies": {
773
  "call-bind-apply-helpers": "^1.0.2",
@@ -792,7 +875,6 @@
792
  "version": "1.0.1",
793
  "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
794
  "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
795
- "dev": true,
796
  "license": "MIT",
797
  "dependencies": {
798
  "dunder-proto": "^1.0.1",
@@ -806,7 +888,6 @@
806
  "version": "1.2.0",
807
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
808
  "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
809
- "dev": true,
810
  "license": "MIT",
811
  "engines": {
812
  "node": ">= 0.4"
@@ -832,7 +913,6 @@
832
  "version": "1.1.0",
833
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
834
  "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
835
- "dev": true,
836
  "license": "MIT",
837
  "engines": {
838
  "node": ">= 0.4"
@@ -841,11 +921,26 @@
841
  "url": "https://github.com/sponsors/ljharb"
842
  }
843
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  "node_modules/hasown": {
845
  "version": "2.0.2",
846
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
847
  "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
848
- "dev": true,
849
  "license": "MIT",
850
  "dependencies": {
851
  "function-bind": "^1.1.2"
@@ -958,12 +1053,34 @@
958
  "version": "1.1.0",
959
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
960
  "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
961
- "dev": true,
962
  "license": "MIT",
963
  "engines": {
964
  "node": ">= 0.4"
965
  }
966
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  "node_modules/moment": {
968
  "version": "2.30.1",
969
  "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
 
11
  "chart.js": "^4.5.0",
12
  "discord.js": "^14.25.1",
13
  "dotenv": "^17.2.3",
14
+ "dropbox": "^10.34.0",
15
  "pdfjs-dist": "^5.6.205",
16
  "pg": "^8.16.3",
17
  "skia-canvas": "^3.0.8",
 
471
  "undici-types": "~7.18.0"
472
  }
473
  },
474
+ "node_modules/@types/node-fetch": {
475
+ "version": "2.6.13",
476
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
477
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
478
+ "license": "MIT",
479
+ "peer": true,
480
+ "dependencies": {
481
+ "@types/node": "*",
482
+ "form-data": "^4.0.4"
483
+ }
484
+ },
485
  "node_modules/@types/ws": {
486
  "version": "8.18.1",
487
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
 
510
  "node": ">= 14"
511
  }
512
  },
513
+ "node_modules/asynckit": {
514
+ "version": "0.4.0",
515
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
516
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
517
+ "license": "MIT",
518
+ "peer": true
519
+ },
520
  "node_modules/bmp-js": {
521
  "version": "0.1.0",
522
  "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
 
546
  "version": "1.0.2",
547
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
548
  "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
 
549
  "license": "MIT",
550
  "dependencies": {
551
  "es-errors": "^1.3.0",
 
584
  "pnpm": ">=8"
585
  }
586
  },
587
+ "node_modules/combined-stream": {
588
+ "version": "1.0.8",
589
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
590
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
591
+ "license": "MIT",
592
+ "peer": true,
593
+ "dependencies": {
594
+ "delayed-stream": "~1.0.0"
595
+ },
596
+ "engines": {
597
+ "node": ">= 0.8"
598
+ }
599
+ },
600
  "node_modules/commander": {
601
  "version": "2.20.3",
602
  "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
 
639
  "url": "https://github.com/sponsors/ljharb"
640
  }
641
  },
642
+ "node_modules/delayed-stream": {
643
+ "version": "1.0.0",
644
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
645
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
646
+ "license": "MIT",
647
+ "peer": true,
648
+ "engines": {
649
+ "node": ">=0.4.0"
650
+ }
651
+ },
652
  "node_modules/detect-libc": {
653
  "version": "2.1.2",
654
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
 
713
  "url": "https://dotenvx.com"
714
  }
715
  },
716
+ "node_modules/dropbox": {
717
+ "version": "10.34.0",
718
+ "resolved": "https://registry.npmjs.org/dropbox/-/dropbox-10.34.0.tgz",
719
+ "integrity": "sha512-5jb5/XzU0fSnq36/hEpwT5/QIep7MgqKuxghEG44xCu7HruOAjPdOb3x0geXv5O/hd0nHpQpWO+r5MjYTpMvJg==",
720
+ "license": "MIT",
721
+ "dependencies": {
722
+ "node-fetch": "^2.6.1"
723
+ },
724
+ "engines": {
725
+ "node": ">=0.10.3"
726
+ },
727
+ "peerDependencies": {
728
+ "@types/node-fetch": "^2.5.7"
729
+ }
730
+ },
731
  "node_modules/dunder-proto": {
732
  "version": "1.0.1",
733
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
734
  "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
 
735
  "license": "MIT",
736
  "dependencies": {
737
  "call-bind-apply-helpers": "^1.0.1",
 
746
  "version": "1.0.1",
747
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
748
  "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
 
749
  "license": "MIT",
750
  "engines": {
751
  "node": ">= 0.4"
 
755
  "version": "1.3.0",
756
  "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
757
  "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
 
758
  "license": "MIT",
759
  "engines": {
760
  "node": ">= 0.4"
 
764
  "version": "1.1.1",
765
  "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
766
  "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
 
767
  "license": "MIT",
768
  "dependencies": {
769
  "es-errors": "^1.3.0"
 
772
  "node": ">= 0.4"
773
  }
774
  },
775
+ "node_modules/es-set-tostringtag": {
776
+ "version": "2.1.0",
777
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
778
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
779
+ "license": "MIT",
780
+ "peer": true,
781
+ "dependencies": {
782
+ "es-errors": "^1.3.0",
783
+ "get-intrinsic": "^1.2.6",
784
+ "has-tostringtag": "^1.0.2",
785
+ "hasown": "^2.0.2"
786
+ },
787
+ "engines": {
788
+ "node": ">= 0.4"
789
+ }
790
+ },
791
  "node_modules/fast-deep-equal": {
792
  "version": "3.1.3",
793
  "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
814
  }
815
  }
816
  },
817
+ "node_modules/form-data": {
818
+ "version": "4.0.5",
819
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
820
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
821
+ "license": "MIT",
822
+ "peer": true,
823
+ "dependencies": {
824
+ "asynckit": "^0.4.0",
825
+ "combined-stream": "^1.0.8",
826
+ "es-set-tostringtag": "^2.1.0",
827
+ "hasown": "^2.0.2",
828
+ "mime-types": "^2.1.12"
829
+ },
830
+ "engines": {
831
+ "node": ">= 6"
832
+ }
833
+ },
834
  "node_modules/function-bind": {
835
  "version": "1.1.2",
836
  "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
837
  "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
 
838
  "license": "MIT",
839
  "funding": {
840
  "url": "https://github.com/sponsors/ljharb"
 
851
  "version": "1.3.0",
852
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
853
  "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
 
854
  "license": "MIT",
855
  "dependencies": {
856
  "call-bind-apply-helpers": "^1.0.2",
 
875
  "version": "1.0.1",
876
  "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
877
  "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
 
878
  "license": "MIT",
879
  "dependencies": {
880
  "dunder-proto": "^1.0.1",
 
888
  "version": "1.2.0",
889
  "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
890
  "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
 
891
  "license": "MIT",
892
  "engines": {
893
  "node": ">= 0.4"
 
913
  "version": "1.1.0",
914
  "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
915
  "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
 
916
  "license": "MIT",
917
  "engines": {
918
  "node": ">= 0.4"
 
921
  "url": "https://github.com/sponsors/ljharb"
922
  }
923
  },
924
+ "node_modules/has-tostringtag": {
925
+ "version": "1.0.2",
926
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
927
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
928
+ "license": "MIT",
929
+ "peer": true,
930
+ "dependencies": {
931
+ "has-symbols": "^1.0.3"
932
+ },
933
+ "engines": {
934
+ "node": ">= 0.4"
935
+ },
936
+ "funding": {
937
+ "url": "https://github.com/sponsors/ljharb"
938
+ }
939
+ },
940
  "node_modules/hasown": {
941
  "version": "2.0.2",
942
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
943
  "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
 
944
  "license": "MIT",
945
  "dependencies": {
946
  "function-bind": "^1.1.2"
 
1053
  "version": "1.1.0",
1054
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1055
  "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
 
1056
  "license": "MIT",
1057
  "engines": {
1058
  "node": ">= 0.4"
1059
  }
1060
  },
1061
+ "node_modules/mime-db": {
1062
+ "version": "1.52.0",
1063
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1064
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1065
+ "license": "MIT",
1066
+ "peer": true,
1067
+ "engines": {
1068
+ "node": ">= 0.6"
1069
+ }
1070
+ },
1071
+ "node_modules/mime-types": {
1072
+ "version": "2.1.35",
1073
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1074
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1075
+ "license": "MIT",
1076
+ "peer": true,
1077
+ "dependencies": {
1078
+ "mime-db": "1.52.0"
1079
+ },
1080
+ "engines": {
1081
+ "node": ">= 0.6"
1082
+ }
1083
+ },
1084
  "node_modules/moment": {
1085
  "version": "2.30.1",
1086
  "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
package.json CHANGED
@@ -15,6 +15,7 @@
15
  "chart.js": "^4.5.0",
16
  "discord.js": "^14.25.1",
17
  "dotenv": "^17.2.3",
 
18
  "pdfjs-dist": "^5.6.205",
19
  "pg": "^8.16.3",
20
  "skia-canvas": "^3.0.8",
 
15
  "chart.js": "^4.5.0",
16
  "discord.js": "^14.25.1",
17
  "dotenv": "^17.2.3",
18
+ "dropbox": "^10.34.0",
19
  "pdfjs-dist": "^5.6.205",
20
  "pg": "^8.16.3",
21
  "skia-canvas": "^3.0.8",
src/config.js CHANGED
@@ -14,6 +14,8 @@ export function getConfig() {
14
  .split(',')
15
  .map((value) => value.trim())
16
  .filter(Boolean);
 
 
17
  const circaDropboxUrl = process.env.CIRCA_DROPBOX_URL?.trim() || null;
18
  const scanReportChannelId = process.env.SCAN_REPORT_CHANNEL_ID?.trim() || null;
19
  const scanAlertChannelId = process.env.SCAN_ALERT_CHANNEL_ID?.trim() || null;
@@ -39,12 +41,19 @@ export function getConfig() {
39
  port,
40
  adminRoleName,
41
  scanner: {
42
- enabled: Boolean(oddsApiKey && circaDropboxUrl && scanReportChannelId && scanAlertChannelId),
 
 
 
 
 
43
  oddsApiKey,
44
  oddsApiBaseUrl,
45
  oddsApiSportKey,
46
  oddsApiRegions,
47
  oddsApiMarkets,
 
 
48
  circaDropboxUrl,
49
  scanReportChannelId,
50
  scanAlertChannelId,
 
14
  .split(',')
15
  .map((value) => value.trim())
16
  .filter(Boolean);
17
+ const dropboxAccessToken = process.env.DROPBOX_ACCESS_TOKEN?.trim() || null;
18
+ const circaDropboxFolderPath = process.env.CIRCA_DROPBOX_FOLDER_PATH?.trim() || null;
19
  const circaDropboxUrl = process.env.CIRCA_DROPBOX_URL?.trim() || null;
20
  const scanReportChannelId = process.env.SCAN_REPORT_CHANNEL_ID?.trim() || null;
21
  const scanAlertChannelId = process.env.SCAN_ALERT_CHANNEL_ID?.trim() || null;
 
41
  port,
42
  adminRoleName,
43
  scanner: {
44
+ enabled: Boolean(
45
+ oddsApiKey
46
+ && scanReportChannelId
47
+ && scanAlertChannelId
48
+ && ((dropboxAccessToken && circaDropboxFolderPath) || circaDropboxUrl)
49
+ ),
50
  oddsApiKey,
51
  oddsApiBaseUrl,
52
  oddsApiSportKey,
53
  oddsApiRegions,
54
  oddsApiMarkets,
55
+ dropboxAccessToken,
56
+ circaDropboxFolderPath,
57
  circaDropboxUrl,
58
  scanReportChannelId,
59
  scanAlertChannelId,
src/embeds.js CHANGED
@@ -402,6 +402,7 @@ export function buildScanStatusEmbed(status) {
402
  { name: 'Min Books / Threshold', value: `${status.minBooks} / ${(status.disagreementThreshold * 100).toFixed(2)}%`, inline: true },
403
  { name: 'Last Scan', value: status.lastScanAt ?? 'Never', inline: true },
404
  { name: 'Last Report', value: status.lastReportAt ?? 'Never', inline: true },
 
405
  { name: 'Last Counts', value: `API: ${status.lastApiEntries ?? 0} | Circa: ${status.lastCircaEntries ?? 0} | Alerts: ${status.lastAlertCount ?? 0}`, inline: false },
406
  { name: 'Last Error', value: status.lastScanError ?? 'None', inline: false },
407
  );
@@ -462,13 +463,25 @@ export function buildCircaDiagnosticEmbed(result) {
462
  return new EmbedBuilder()
463
  .setColor(PALETTE.gold)
464
  .setTitle('Circa OCR Diagnostic')
465
- .setDescription(`Parsed **${result.totalEntries}** Circa entries from the latest download.`)
466
  .addFields(
467
  { name: 'Parsed Preview', value: truncate(preview, 1000), inline: false },
468
  { name: 'OCR Text Sample', value: truncate(result.rawTextSample || 'No OCR text extracted.', 1000), inline: false },
469
  );
470
  }
471
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  function escapeCsv(value) {
473
  const stringValue = String(value);
474
  if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
 
402
  { name: 'Min Books / Threshold', value: `${status.minBooks} / ${(status.disagreementThreshold * 100).toFixed(2)}%`, inline: true },
403
  { name: 'Last Scan', value: status.lastScanAt ?? 'Never', inline: true },
404
  { name: 'Last Report', value: status.lastReportAt ?? 'Never', inline: true },
405
+ { name: 'Last Circa File', value: status.lastCircaFileName ?? 'None', inline: false },
406
  { name: 'Last Counts', value: `API: ${status.lastApiEntries ?? 0} | Circa: ${status.lastCircaEntries ?? 0} | Alerts: ${status.lastAlertCount ?? 0}`, inline: false },
407
  { name: 'Last Error', value: status.lastScanError ?? 'None', inline: false },
408
  );
 
463
  return new EmbedBuilder()
464
  .setColor(PALETTE.gold)
465
  .setTitle('Circa OCR Diagnostic')
466
+ .setDescription(`Parsed **${result.totalEntries}** Circa entries from **${result.fileName ?? 'the latest Circa file'}**.`)
467
  .addFields(
468
  { name: 'Parsed Preview', value: truncate(preview, 1000), inline: false },
469
  { name: 'OCR Text Sample', value: truncate(result.rawTextSample || 'No OCR text extracted.', 1000), inline: false },
470
  );
471
  }
472
 
473
+ export function buildCircaFailureEmbed(details) {
474
+ return new EmbedBuilder()
475
+ .setColor(PALETTE.red)
476
+ .setTitle('Circa Ingestion Failed')
477
+ .setDescription('The market scanner could not parse the current Circa file, so no Circa-based alerts were posted for this run.')
478
+ .addFields(
479
+ { name: 'File', value: details.fileName ?? 'Unknown', inline: true },
480
+ { name: 'Reason', value: details.reason ?? 'Unknown error', inline: true },
481
+ { name: 'Source', value: details.source ?? 'Dropbox API', inline: true },
482
+ );
483
+ }
484
+
485
  function escapeCsv(value) {
486
  const stringValue = String(value);
487
  if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
src/index.js CHANGED
@@ -34,6 +34,7 @@ import {
34
  buildCommandsEmbed,
35
  buildCircaDiagnosticEmbed,
36
  buildCircaAlertEmbed,
 
37
  buildDeleteBetEmbed,
38
  buildEditBetEmbed,
39
  buildErrorEmbed,
@@ -84,6 +85,7 @@ async function main() {
84
  embeds: {
85
  buildMarketTopEmbed,
86
  buildCircaAlertEmbed,
 
87
  },
88
  logger: console,
89
  });
 
34
  buildCommandsEmbed,
35
  buildCircaDiagnosticEmbed,
36
  buildCircaAlertEmbed,
37
+ buildCircaFailureEmbed,
38
  buildDeleteBetEmbed,
39
  buildEditBetEmbed,
40
  buildErrorEmbed,
 
85
  embeds: {
86
  buildMarketTopEmbed,
87
  buildCircaAlertEmbed,
88
+ buildCircaFailureEmbed,
89
  },
90
  logger: console,
91
  });
src/market-scanner.js CHANGED
@@ -1,4 +1,5 @@
1
  import { Canvas } from 'skia-canvas';
 
2
  import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
3
  import { createWorker } from 'tesseract.js';
4
 
@@ -374,6 +375,25 @@ export function analyzeMarkets(entries, config = {}) {
374
  };
375
  }
376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  async function fetchArrayBuffer(url) {
378
  const response = await fetch(url);
379
  if (!response.ok) {
@@ -390,6 +410,53 @@ async function fetchJson(url) {
390
  return response.json();
391
  }
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  async function extractPdfText(buffer) {
394
  const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(buffer), useWorkerFetch: false, isEvalSupported: false });
395
  const pdf = await loadingTask.promise;
@@ -432,9 +499,23 @@ async function recognizeTextFromImage(imageBuffer) {
432
  }
433
 
434
  export async function fetchCircaEntries(config) {
435
- const buffer = await fetchArrayBuffer(config.circaDropboxUrl);
436
- const text = await extractPdfText(buffer);
 
 
 
 
 
 
 
 
 
 
 
 
437
  return {
 
 
438
  text,
439
  entries: parseCircaOcrText(text),
440
  };
@@ -516,6 +597,7 @@ export class MarketScanner {
516
  lastCircaEntries: 0,
517
  lastApiEntries: 0,
518
  lastAlertCount: 0,
 
519
  };
520
  }
521
 
@@ -655,6 +737,7 @@ export class MarketScanner {
655
  async runCircaDiagnostic() {
656
  const result = await fetchCircaEntries(this.config);
657
  return {
 
658
  rawTextSample: result.text.slice(0, 1000),
659
  parsedEntries: result.entries.slice(0, 10),
660
  totalEntries: result.entries.length,
@@ -683,13 +766,32 @@ export class MarketScanner {
683
  this.status.lastScanError = null;
684
  this.status.lastApiEntries = oddsEntries.length;
685
  this.status.lastCircaEntries = circaResult.entries.length;
 
686
 
687
  return analysis;
688
  } catch (error) {
689
  this.status.lastScanError = error.message;
 
 
 
690
  throw error;
691
  } finally {
692
  this.running = false;
693
  }
694
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  }
 
1
  import { Canvas } from 'skia-canvas';
2
+ import { Dropbox } from 'dropbox';
3
  import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
4
  import { createWorker } from 'tesseract.js';
5
 
 
375
  };
376
  }
377
 
378
+ export function matchesCircaMlbFilename(name) {
379
+ return /^MLB\s+Props\s*-\s*\d{4}-\d{1,2}-\d{1,2}\.pdf$/i.test(normalizeWhitespace(name));
380
+ }
381
+
382
+ export function selectLatestCircaFile(entries = []) {
383
+ const candidates = entries
384
+ .filter((entry) => entry['.tag'] === 'file')
385
+ .filter((entry) => /\.pdf$/i.test(entry.name))
386
+ .filter((entry) => matchesCircaMlbFilename(entry.name));
387
+
388
+ if (candidates.length === 0) {
389
+ return null;
390
+ }
391
+
392
+ return candidates.sort((left, right) =>
393
+ new Date(right.server_modified).getTime() - new Date(left.server_modified).getTime()
394
+ )[0];
395
+ }
396
+
397
  async function fetchArrayBuffer(url) {
398
  const response = await fetch(url);
399
  if (!response.ok) {
 
410
  return response.json();
411
  }
412
 
413
+ function getDropboxClient(config) {
414
+ if (!config.dropboxAccessToken) {
415
+ throw new Error('Missing DROPBOX_ACCESS_TOKEN for Circa Dropbox API access.');
416
+ }
417
+
418
+ return new Dropbox({
419
+ accessToken: config.dropboxAccessToken,
420
+ fetch,
421
+ });
422
+ }
423
+
424
+ async function downloadDropboxFile(config) {
425
+ const client = getDropboxClient(config);
426
+ const folderPath = config.circaDropboxFolderPath;
427
+ if (!folderPath) {
428
+ throw new Error('Missing CIRCA_DROPBOX_FOLDER_PATH for Circa Dropbox API access.');
429
+ }
430
+
431
+ const listResponse = await client.filesListFolder({
432
+ path: folderPath,
433
+ });
434
+ const selectedFile = selectLatestCircaFile(listResponse.result.entries ?? []);
435
+ if (!selectedFile) {
436
+ throw new Error(`No matching MLB Props PDF found in Dropbox folder ${folderPath}.`);
437
+ }
438
+
439
+ const downloadResponse = await client.filesDownload({
440
+ path: selectedFile.path_lower ?? selectedFile.path_display,
441
+ });
442
+
443
+ const binary = downloadResponse.result.fileBinary;
444
+ let buffer;
445
+ if (binary instanceof Blob) {
446
+ buffer = await binary.arrayBuffer();
447
+ } else if (binary?.buffer) {
448
+ buffer = binary.buffer.slice(binary.byteOffset, binary.byteOffset + binary.byteLength);
449
+ } else {
450
+ buffer = binary;
451
+ }
452
+
453
+ return {
454
+ fileName: selectedFile.name,
455
+ buffer,
456
+ source: 'Dropbox API',
457
+ };
458
+ }
459
+
460
  async function extractPdfText(buffer) {
461
  const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(buffer), useWorkerFetch: false, isEvalSupported: false });
462
  const pdf = await loadingTask.promise;
 
499
  }
500
 
501
  export async function fetchCircaEntries(config) {
502
+ let sourceFile;
503
+ if (config.dropboxAccessToken && config.circaDropboxFolderPath) {
504
+ sourceFile = await downloadDropboxFile(config);
505
+ } else if (config.circaDropboxUrl) {
506
+ sourceFile = {
507
+ fileName: 'Circa shared-link file',
508
+ buffer: await fetchArrayBuffer(config.circaDropboxUrl),
509
+ source: 'Shared link',
510
+ };
511
+ } else {
512
+ throw new Error('No Circa source configured. Set Dropbox or shared-link Circa config.');
513
+ }
514
+
515
+ const text = await extractPdfText(sourceFile.buffer);
516
  return {
517
+ fileName: sourceFile.fileName,
518
+ source: sourceFile.source,
519
  text,
520
  entries: parseCircaOcrText(text),
521
  };
 
597
  lastCircaEntries: 0,
598
  lastApiEntries: 0,
599
  lastAlertCount: 0,
600
+ lastCircaFileName: null,
601
  };
602
  }
603
 
 
737
  async runCircaDiagnostic() {
738
  const result = await fetchCircaEntries(this.config);
739
  return {
740
+ fileName: result.fileName,
741
  rawTextSample: result.text.slice(0, 1000),
742
  parsedEntries: result.entries.slice(0, 10),
743
  totalEntries: result.entries.length,
 
766
  this.status.lastScanError = null;
767
  this.status.lastApiEntries = oddsEntries.length;
768
  this.status.lastCircaEntries = circaResult.entries.length;
769
+ this.status.lastCircaFileName = circaResult.fileName ?? null;
770
 
771
  return analysis;
772
  } catch (error) {
773
  this.status.lastScanError = error.message;
774
+ if (String(error.message ?? '').toLowerCase().includes('pdf') || String(error.message ?? '').toLowerCase().includes('ocr') || String(error.message ?? '').toLowerCase().includes('dropbox')) {
775
+ await this.sendCircaFailureAlert(error);
776
+ }
777
  throw error;
778
  } finally {
779
  this.running = false;
780
  }
781
  }
782
+
783
+ async sendCircaFailureAlert(error) {
784
+ const channel = await this.client.channels.fetch(this.config.scanAlertChannelId).catch(() => null);
785
+ if (!channel?.isTextBased()) {
786
+ return;
787
+ }
788
+
789
+ await channel.send({
790
+ embeds: [this.embeds.buildCircaFailureEmbed({
791
+ fileName: this.status.lastCircaFileName,
792
+ reason: error.message,
793
+ source: this.config.dropboxAccessToken ? 'Dropbox API' : 'Shared link',
794
+ })],
795
+ }).catch(() => null);
796
+ }
797
  }
test/market-scanner.test.js CHANGED
@@ -5,8 +5,10 @@ import {
5
  analyzeMarkets,
6
  buildMarketKey,
7
  fetchOddsApiEntries,
 
8
  normalizeOddsApiEntries,
9
  parseCircaOcrText,
 
10
  } from '../src/market-scanner.js';
11
 
12
  test('converts american odds to implied probability', () => {
@@ -165,3 +167,32 @@ test('fetches odds api entries from event-level endpoints', async () => {
165
  global.fetch = originalFetch;
166
  }
167
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  analyzeMarkets,
6
  buildMarketKey,
7
  fetchOddsApiEntries,
8
+ matchesCircaMlbFilename,
9
  normalizeOddsApiEntries,
10
  parseCircaOcrText,
11
+ selectLatestCircaFile,
12
  } from '../src/market-scanner.js';
13
 
14
  test('converts american odds to implied probability', () => {
 
167
  global.fetch = originalFetch;
168
  }
169
  });
170
+
171
+ test('matches expected circa mlb filenames', () => {
172
+ assert.equal(matchesCircaMlbFilename('MLB Props - 2026-4-3.pdf'), true);
173
+ assert.equal(matchesCircaMlbFilename('MLB Props - 2026-04-03.pdf'), true);
174
+ assert.equal(matchesCircaMlbFilename('NBA Props - 2026-4-3.pdf'), false);
175
+ });
176
+
177
+ test('selects newest matching circa mlb pdf', () => {
178
+ const selected = selectLatestCircaFile([
179
+ {
180
+ '.tag': 'file',
181
+ name: 'MLB Props - 2026-4-2.pdf',
182
+ server_modified: '2026-04-02T12:00:00Z',
183
+ },
184
+ {
185
+ '.tag': 'file',
186
+ name: 'MLB Props - 2026-4-3.pdf',
187
+ server_modified: '2026-04-03T12:00:00Z',
188
+ },
189
+ {
190
+ '.tag': 'file',
191
+ name: 'NBA Props - 2026-4-3.pdf',
192
+ server_modified: '2026-04-04T12:00:00Z',
193
+ },
194
+ ]);
195
+
196
+ assert.ok(selected);
197
+ assert.equal(selected.name, 'MLB Props - 2026-4-3.pdf');
198
+ });