├── .gitignore ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── apps ├── pushnotification │ ├── constants.py │ ├── msg_formatter.py │ └── smtp.py ├── startup │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── storage │ ├── base.py │ ├── query.py │ └── storage.py ├── ticketscraping │ ├── README.md │ ├── connection │ │ ├── asyn_tasks_receiver.py │ │ ├── mail_receiver.py │ │ ├── receiver.py │ │ ├── receiver_process.py │ │ └── sender.py │ ├── constants.py │ ├── js │ │ ├── injector-header.js │ │ ├── injector.js │ │ ├── package-lock.json │ │ └── package.json │ ├── models │ │ └── pick.py │ ├── prepare_reese84token.py │ ├── schedulers │ │ ├── async_tasks_scheduler.py │ │ └── mail_scheduler.py │ ├── scraping.py │ ├── seat_analysis.py │ └── tasks │ │ ├── asynchronous.py │ │ ├── periodic.py │ │ ├── strategies │ │ └── quarters_seats.py │ │ └── util │ │ ├── decorators.py │ │ └── math.py └── trackerapi │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── error_handler.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── buildspec.yml ├── manage.py ├── secret.py ├── tmtracker ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | /**/__pycache__ 2 | /**/js/node_modules 3 | /**/js/antibot-simulation.js 4 | /**/tmp 5 | /**/.DS_Store 6 | .vscode 7 | docker-compose.yml 8 | entrypoint.sh 9 | prod.sh 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | ARG DJANGO_ENV 3 | 4 | ENV DJANGO_ENV=${DJANGO_ENV} 5 | # PIPENV_DOTENV_LOCATION=config/.env 6 | 7 | # Creating folders, and files for a project: 8 | COPY . /code 9 | WORKDIR /code 10 | RUN apk update 11 | RUN apk add --no-cache \ 12 | build-base \ 13 | g++ \ 14 | cairo-dev \ 15 | pango-dev \ 16 | nodejs \ 17 | npm 18 | 19 | # js install 20 | RUN npm run build --prefix /code/apps/ticketscraping/js 21 | 22 | # python install 23 | RUN pip install pipenv 24 | RUN pipenv install $(test "$DJANGO_ENV" == production || echo "--dev") --deploy --system --ignore-pipfile 25 | 26 | # start the server 27 | CMD ["gunicorn", "tmtracker.wsgi:application", "-b", "0.0.0.0:8080", "--log-file=/log/message.log"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jack Li and Frank Gao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "*" 8 | pymongo = "*" 9 | dnspython = "*" 10 | requests = "*" 11 | python-dateutil = "*" 12 | 13 | [dev-packages] 14 | gunicorn = "*" 15 | 16 | [requires] 17 | python_version = "3.9" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3d99dcb55d3173e7391ed2e56f6784ea7a1c71007acf7d209b32b6d936f9e6c6" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", 22 | "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==3.5.2" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", 30 | "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" 31 | ], 32 | "markers": "python_version >= '3.6'", 33 | "version": "==2022.6.15" 34 | }, 35 | "charset-normalizer": { 36 | "hashes": [ 37 | "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", 38 | "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==2.1.0" 42 | }, 43 | "django": { 44 | "hashes": [ 45 | "sha256:a67a793ff6827fd373555537dca0da293a63a316fe34cb7f367f898ccca3c3ae", 46 | "sha256:ca54ebedfcbc60d191391efbf02ba68fb52165b8bf6ccd6fe71f098cac1fe59e" 47 | ], 48 | "index": "pypi", 49 | "version": "==4.0.6" 50 | }, 51 | "dnspython": { 52 | "hashes": [ 53 | "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", 54 | "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" 55 | ], 56 | "index": "pypi", 57 | "version": "==2.2.1" 58 | }, 59 | "idna": { 60 | "hashes": [ 61 | "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", 62 | "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" 63 | ], 64 | "markers": "python_version >= '3.5'", 65 | "version": "==3.3" 66 | }, 67 | "pymongo": { 68 | "hashes": [ 69 | "sha256:019a4c13ef1d9accd08de70247068671b116a0383adcd684f6365219f29f41cd", 70 | "sha256:07f50a3b8a3afb086089abcd9ab562fb2a27b63fd7017ca13dfe7b663c8f3762", 71 | "sha256:08a619c92769bd7346434dfc331a3aa8dc63bee80ed0be250bb0e878c69a6f3e", 72 | "sha256:0a3474e6a0df0077a44573727341df6627042df5ca61ea5373c157bb6512ccc7", 73 | "sha256:0b8a1c766de29173ddbd316dbd75a97b19a4cf9ac45a39ad4f53426e5df1483b", 74 | "sha256:0f7e3872fb7b61ec574b7e04302ea03928b670df583f8691cb1df6e54cd42b19", 75 | "sha256:17df40753085ccba38a0e150001f757910d66440d9b5deced30ed4cc8b45b6f3", 76 | "sha256:298908478d07871dbe17e9ccd37a10a27ad3f37cc1faaf0cc4d205da3c3e8539", 77 | "sha256:302ac0f4825501ab0900b8f1a2bb2dc7d28f69c7f15fbc799fb26f9b9ebb1ecb", 78 | "sha256:303d1b3da2461586379d98b344b529598c8156857285ba5bd156dab1c875d1f6", 79 | "sha256:306336dab4537b2343e52ec34017c3051c3aee5a961fff4915ab27f7e6d9b1e9", 80 | "sha256:30d35a8855f328a85e5002f0908b24e500efdf8f5f78b73098995ce111baa2a9", 81 | "sha256:3139c9ddee379c22a9109a0b3bf4cdb64597db2bbd3909f7a2825b47226977a4", 82 | "sha256:32e785c37f6a0e844788c6085ea2c9c0c528348c22cebe91896705a92f2b1b26", 83 | "sha256:33a5693e8d1fbb7743b7e867d43c1095652a0c6fedddab6cefe6020bee2ca393", 84 | "sha256:35d02603c2318676fca5049cdc722bb2e7a378eaccf139ad767365e0eb3bcdbe", 85 | "sha256:4516a5ce2beaebddc74d6e304ed520324dda99573c310ef4078284b026f81e93", 86 | "sha256:49bb36986f11da2da190a2e777a411c0a28eeb8623850091ea8099b84e3860c7", 87 | "sha256:4aa4800530782f7d38aeb169476a5bc692aacc394686f0ca3866e4bb85c9aa3f", 88 | "sha256:4d1cdece06156542c18b691511a01fe78a694b9fa287ffd8e15680dbf2beeed5", 89 | "sha256:4e4d2babb8737d650250d0fa940ffa1b88aa92b8eb399af093734950a1eeca45", 90 | "sha256:4fd5c4f25d8d488ee5701c3ec786f52907dca653b47ce8709bcc2bfb0f5506ae", 91 | "sha256:52c8b7bffd2140818ade2aa28c24cfe47935a7273a3bb976d1d8fb17e716536f", 92 | "sha256:56b856a459762a3c052987e28ed2bd4b874f0be6671d2cc4f74c4891f47f997a", 93 | "sha256:571a3e1ef4abeb4ac719ac381f5aada664627b4ee048d9995e93b4bcd0f70601", 94 | "sha256:5cae9c935cdc53e4729920543b7d990615a115d85f32144773bc4b2b05144628", 95 | "sha256:5d6ef3fa41f3e3be93483a77f81dea8c7ce5ed4411382a31af2b09b9ec5d9585", 96 | "sha256:6396f0db060db9d8751167ea08f3a77a41a71cd39236fade4409394e57b377e8", 97 | "sha256:69beffb048de19f7c18617b90e38cbddfac20077b1826c27c3fe2e3ef8ac5a43", 98 | "sha256:7507439cd799295893b5602f438f8b6a0f483efb00720df1aa33a39102b41bcf", 99 | "sha256:7aa40509dd9f75c256f0a7533d5e2ccef711dbbf0d91c13ac937d21d76d71656", 100 | "sha256:7d69a3d980ecbf7238ab37b9027c87ad3b278bb3742a150fc33b5a8a9d990431", 101 | "sha256:7dae2cf84a09329617b08731b95ad1fc98d50a9b40c2007e351438bd119a2f7a", 102 | "sha256:7f36eacc70849d40ce86c85042ecfcbeab810691b1a3b08062ede32a2d6521ac", 103 | "sha256:7f55a602d55e8f0feafde533c69dfd29bf0e54645ab0996b605613cda6894a85", 104 | "sha256:8357aa727094798f1d831339ecfd8b3e388c01db6015a3cbd51790cb75e39994", 105 | "sha256:84dc6bfeaeba98fe93fc837b12f9af4842694cdbde18083f150e80aec3de88f9", 106 | "sha256:86b18420f00d5977bda477369ac85e04185ef94046a04ae0d85f5a807d1a8eb4", 107 | "sha256:89f32d8450e15b0c11efdc81e2704d68c502c889d48415a50add9fa031144f75", 108 | "sha256:8a1de8931cdad8cd12724e12a6167eef8cb478cc3ee5d2c9f4670c934f2975e1", 109 | "sha256:8f106468062ac7ff03e3522a66cb7b36c662326d8eb7af1be0f30563740ff002", 110 | "sha256:9a4ea87a0401c06b687db29e2ae836b2b58480ab118cb6eea8ac2ef45a4345f8", 111 | "sha256:9ee1b019a4640bf39c0705ab65e934cfe6b89f1a8dc26f389fae3d7c62358d6f", 112 | "sha256:a0d7c6d6fbca62508ea525abd869fca78ecf68cd3bcf6ae67ec478aa37cf39c0", 113 | "sha256:a1417cb339a367a5dfd0e50193a1c0e87e31325547a0e7624ee4ff414c0b53b3", 114 | "sha256:a35f1937b0560587d478fd2259a6d4f66cf511c9d28e90b52b183745eaa77d95", 115 | "sha256:a4a35e83abfdac7095430e1c1476e0871e4b234e936f4a7a7631531b09a4f198", 116 | "sha256:a7d1c8830a7bc10420ceb60a256d25ab5b032a6dad12a46af6ab2e470cee9124", 117 | "sha256:a938d4d5b530f8ea988afb80817209eabc150c53b8c7af79d40080313a35e470", 118 | "sha256:a9a2c377106fe01a57bad0f703653de286d56ee5285ed36c6953535cfa11f928", 119 | "sha256:baf7546afd27be4f96f23307d7c295497fb512875167743b14a7457b95761294", 120 | "sha256:bb21e2f35d6f09aa4a6df0c716f41e036cfcf05a98323b50294f93085ad775e9", 121 | "sha256:bc62ba37bcb42e4146b853940b65a2de31c2962d2b6da9bc3ce28270d13b5c4e", 122 | "sha256:be3ba736aabf856195199208ed37459408c932940cbccd2dc9f6ff2e800b0261", 123 | "sha256:c03eb43d15c8af58159e7561076634d565530aaacaf48cf4e070c3501e88a372", 124 | "sha256:c1349331fa743eed4042f9652200e60596f8beb957554acbcbb42aad4272c606", 125 | "sha256:c3637cfce519560e2a2579d05eb81e912d109283b8ddc8de46f57ec20d273d92", 126 | "sha256:c481cd1af2a77f58f495f7f87c2d715c6f1179d07c1ec927cca1f7977a2d99aa", 127 | "sha256:c575f9499e5f540e034ff87bef894f031ae613a98b0d1d3afcc1f482527d5f1c", 128 | "sha256:c604831daf2e7e5979ecd97a90cb8c4a7bae208ff45bc792e32eae09c3281afb", 129 | "sha256:c759e1e0333664831d8d1d6b26cf59f23f3707758f696c71f506504b33130f81", 130 | "sha256:c8a2743dd50629c0222f26c5f55975e45841d985b4b1c7a54b3f03b53de3427d", 131 | "sha256:cbcac9263f500da94405cc9fc7e7a42a3ba6c2fe88b2cd7039737cba44c66889", 132 | "sha256:cce1b7a680653e31ff2b252f19a39f1ded578a35a96c419ddb9632c62d2af7d8", 133 | "sha256:cf96799b3e5e2e2f6dbca015f72b28e7ae415ce8147472f89a3704a035d6336d", 134 | "sha256:d06ed18917dbc7a938c4231cbbec52a7e474be270b2ef9208abb4d5a34f5ceb9", 135 | "sha256:d4ba5b4f1a0334dbe673f767f28775744e793fcb9ea57a1d72bc622c9f90e6b4", 136 | "sha256:d7b8f25c9b0043cbaf77b8b895814e33e7a3c807a097377c07e1bd49946030d5", 137 | "sha256:d86511ef8217822fb8716460aaa1ece31fe9e8a48900e541cb35acb7c35e9e2e", 138 | "sha256:db8a9cbe965c7343feab2e2bf9a3771f303f8a7ca401dececb6ef28e06b3b18c", 139 | "sha256:dbe92a8808cefb284e235b8f82933d7d2e24ff929fe5d53f1fd3ca55fced4b58", 140 | "sha256:deb83cc9f639045e2febcc8d4306d4b83893af8d895f2ed70aa342a3430b534c", 141 | "sha256:df9084e06efb3d59608a6a443faa9861828585579f0ae8e95f5a4dab70f1a00f", 142 | "sha256:dfb89e92746e4a1e0d091cba73d6cc1e16b4094ebdbb14c2e96a80320feb1ad7", 143 | "sha256:e13ddfe2ead9540e8773cae098f54c5206d6fcef64846a3e5042db47fc3a41ed", 144 | "sha256:e4956384340eec7b526149ac126c8aa11d32441cb3ce77a690cb4821d0d0635c", 145 | "sha256:e6eecd027b6ba5617ea6af3e12e20d578d8f4ad1bf51a9abe69c6fd4835ea532", 146 | "sha256:eff9818b7671a55f1ce781398607e0d8c304cd430c0581fbe15b868a7a371c27", 147 | "sha256:f0aea377b9dfc166c8fa05bb158c30ee3d53d73f0ed2fc05ba6c638d9563422f", 148 | "sha256:f1fba193ab2f25849e24caa4570611aa2f80bc1c1ba791851523734b4ed69e43", 149 | "sha256:f6db4f00d3baad615e99a865539391243d12b113fb628ebda1d7794ce02d5a10", 150 | "sha256:f9405c02af86850e0a8a8ba777b7e7609e0d07bff46adc4f78892cc2d5456018", 151 | "sha256:fb4445e3721720c5ca14c0650f35c263b3430e6e16df9d2504618df914b3fb99" 152 | ], 153 | "index": "pypi", 154 | "version": "==4.1.1" 155 | }, 156 | "python-dateutil": { 157 | "hashes": [ 158 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 159 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 160 | ], 161 | "index": "pypi", 162 | "version": "==2.8.2" 163 | }, 164 | "requests": { 165 | "hashes": [ 166 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 167 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 168 | ], 169 | "index": "pypi", 170 | "version": "==2.28.1" 171 | }, 172 | "six": { 173 | "hashes": [ 174 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 175 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 176 | ], 177 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 178 | "version": "==1.16.0" 179 | }, 180 | "sqlparse": { 181 | "hashes": [ 182 | "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", 183 | "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" 184 | ], 185 | "markers": "python_version >= '3.5'", 186 | "version": "==0.4.2" 187 | }, 188 | "urllib3": { 189 | "hashes": [ 190 | "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", 191 | "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" 192 | ], 193 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", 194 | "version": "==1.26.10" 195 | } 196 | }, 197 | "develop": { 198 | "gunicorn": { 199 | "hashes": [ 200 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 201 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 202 | ], 203 | "index": "pypi", 204 | "version": "==20.1.0" 205 | }, 206 | "setuptools": { 207 | "hashes": [ 208 | "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", 209 | "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" 210 | ], 211 | "markers": "python_version >= '3.7'", 212 | "version": "==63.1.0" 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ticketmaster Ticket Tracker API 2 | ![build badge](https://codebuild.us-east-2.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiRm1sYWFwSjhaSDA1SWVOR2ZLZlcxc2FoVlp6UUNQQ2pjdDNQYnVobkFnblR4WmdDSWwzaXdTL1JFRy9SUmQxWThCYkR6YUdtR04vN3grZmdlSWFMV2hNPSIsIml2UGFyYW1ldGVyU3BlYyI6IlZUSTJ2bWwvYWZDWWZqdHEiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=main)\ 3 | Serves as the back end of concert ticket price tracking based on Ticketmaster real-time best seats. 4 | 5 | ### Installation 6 | - Dev environment 7 | - To install Python dependencies, run `pipenv install`. 8 | - You must have preinstalled Node.js. 9 | - To install node dependencies, run `yarn -cwd ticketscraping/js`. 10 | - If __node-canvas__ in `node_modules` fails to install, please check if you have the correct prerequisite softwares installed on your machine. The detail can be found at https://github.com/Automattic/node-canvas/wiki#installation-guides based on your system. 11 | - Prod environment 12 | - Use Dockerfile to build a container image. 13 | 14 | ### August 1, 2023 Update 15 | Reese84 code stopped working as TicketMaster implement a new anti-bot mechanism as of Aug 1. We will try to get it fixed. 16 | -------------------------------------------------------------------------------- /apps/pushnotification/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | port = 465 # For starttls 4 | smtp_server = "smtp.gmail.com" 5 | sender_email = "noreply.ticketmasterbestseat@gmail.com" 6 | subject = "Message from Ticketmaster Ticket Tracker" 7 | app_password = os.environ.get('MAILER_PW', '') 8 | -------------------------------------------------------------------------------- /apps/pushnotification/msg_formatter.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from ..ticketscraping.models.pick import Pick 3 | 4 | def price_formatter(price): 5 | price_str = '' 6 | if type(price) is float or int: 7 | price_str = "$" + "{:.2f}".format(price) 8 | elif type(price) is str: 9 | price_str = price 10 | return price_str 11 | 12 | def decimal_to_percent(num: float): 13 | return "{:.2f}".format(num*100) + "%" 14 | 15 | def format_date(date: datetime): 16 | return date.isoformat() 17 | 18 | def default_formatter(s: str): 19 | return s 20 | 21 | def format_seat_columns(cols): 22 | if type(cols) is str: 23 | return cols 24 | elif type(cols) is list: 25 | return "(" + ",".join(cols) + ")" 26 | return '-' 27 | 28 | def apply_format(s, formatter)->str: 29 | return formatter(s) 30 | 31 | def apply(values: list, formatters: list, delimiter="\t"): 32 | if len(values) != len(formatters): 33 | raise Exception('values and formatters must have the same length') 34 | s = [] 35 | for i in range(len(values)): 36 | s.append(apply_format(values[i], formatters[i])) 37 | return delimiter.join(s) 38 | 39 | def format_full_seat(seat: dict, delimiter="\t"): 40 | price = seat.get("price", "n/a") 41 | section = seat.get("section", "n/a") 42 | row = seat.get("row", "n/a") 43 | seat_columns = seat.get("seat_columns", "n/a") 44 | last_modified = seat.get("last_modified", "n/a") 45 | return apply( 46 | [price, section, row, seat_columns, last_modified], 47 | [price_formatter, default_formatter, default_formatter, 48 | format_seat_columns, format_date], 49 | delimiter) 50 | 51 | def format_price_only_seat(seat: dict, delimiter="\t"): 52 | price = seat.get("price", "n/a") 53 | last_modified = seat.get("last_modified", "n/a") 54 | return apply([price, last_modified], [price_formatter, format_date], delimiter) 55 | 56 | def format_seat(seat: dict, price_only=False, delimiter="\t"): 57 | if price_only: 58 | return format_price_only_seat(seat, delimiter) 59 | else: 60 | return format_full_seat(seat, delimiter) 61 | 62 | def format_seats(seats: list, price_only=False, delimiter="\t"): 63 | return "\n".join([format_seat(seat, price_only, delimiter) for seat in seats]) 64 | 65 | 66 | def format_entire_mail(pick: Pick, target_price: int, percentile: float, rank: int, num_total: int, top_history_seats: list, same_seats: list): 67 | """ 68 | structure of message: 69 | 1. greetings 70 | 2. attributes of new seats 71 | 3. top 3 comparable history seats 72 | 4. exact same seats if possible 73 | 5. signature 74 | """ 75 | p1 = ( 76 | f"Hi!" 77 | ) 78 | p2 = ( 79 | f"Congratulations! Ticket tracker reminds you that your ticket subscription request with target price {price_formatter(target_price)} " 80 | f"found better budget seats (price, section, row, seats) at ({format_full_seat(vars(pick), delimiter=', ')}). " 81 | f"{decimal_to_percent(percentile)} of all comparable seats in the history are better than the newly found seats, that is, " 82 | f"they rank no.{rank} out of {num_total} comparable seats in the history." 83 | ) 84 | p3 = ( 85 | f"You can compare to history seats that are better than the newly found seats:" 86 | f"{chr(10)}" 87 | f"{format_seats(top_history_seats, price_only=False)}" 88 | ) if len(top_history_seats) > 0 else "" 89 | p4 = ( 90 | f"The newly found seats have history prices:" 91 | f"{chr(10)}" 92 | f"{format_seats(same_seats, price_only=True)}" 93 | ) if len(same_seats) > 0 else "" 94 | p5 = ( 95 | f"Bests," 96 | f"{chr(10)}" 97 | f"Ticketmaster Ticket Tracker" 98 | ) 99 | paras = list(filter(lambda p: len(p) > 0, [p1, p2, p3, p4, p5])) 100 | return "\n\n".join(paras) 101 | -------------------------------------------------------------------------------- /apps/pushnotification/smtp.py: -------------------------------------------------------------------------------- 1 | from smtplib import SMTP_SSL 2 | from ssl import create_default_context 3 | from email.message import EmailMessage 4 | from . import constants 5 | 6 | 7 | def init_server(): 8 | context = create_default_context() 9 | server = SMTP_SSL(constants.smtp_server, constants.port, context=context) 10 | return server 11 | 12 | 13 | def server_login(server: SMTP_SSL, password: str): 14 | return server.login(constants.sender_email, password) 15 | 16 | 17 | def server_send_email(server: SMTP_SSL, receiver_emails: list[str], message: str): 18 | em = EmailMessage() 19 | em['From'] = constants.sender_email 20 | em['To'] = receiver_emails 21 | em['subject'] = constants.subject 22 | 23 | em.set_content(message) 24 | return server.sendmail(constants.sender_email, receiver_emails, em.as_string()) 25 | 26 | 27 | def send_email(receiver_emails: list[str], messages: list[str]): 28 | global server 29 | if len(messages) == 0: 30 | return 31 | # print(receiver_emails, messages[0]) 32 | try: 33 | err = server_send_email(server, receiver_emails, messages[0]) 34 | if err is not None: 35 | raise Exception('could not send email to the receiver') 36 | except Exception as ex: 37 | print(ex) 38 | 39 | 40 | server = init_server() 41 | 42 | 43 | def auth_server(): 44 | global server 45 | server_login(server, constants.app_password) 46 | -------------------------------------------------------------------------------- /apps/startup/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'apps.startup.apps.MyAppConfig' 2 | -------------------------------------------------------------------------------- /apps/startup/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/startup/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from datetime import datetime 3 | from threading import Thread 4 | from multiprocessing import Process 5 | 6 | 7 | def run_prepare(): 8 | # import module inside the child process to prevent execution in the parent process 9 | print( 10 | f"ticket scraping service started at {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}") 11 | 12 | # start sender socket 13 | from apps.ticketscraping.schedulers.async_tasks_scheduler import async_tasks_scheduler 14 | conn_thread = Thread(target=async_tasks_scheduler.connect) 15 | conn_thread.start() 16 | # wait for async tasks handler to connect 17 | conn_thread.join() 18 | 19 | # start itself (scraping) 20 | from apps.ticketscraping.scraping import start 21 | start() 22 | 23 | 24 | def run(): 25 | # starter 26 | p = Process(target=run_prepare, daemon=True) 27 | p.start() 28 | # start receiver socket 29 | from apps.ticketscraping.connection.asyn_tasks_receiver import run 30 | conn_process = Process(target=run) 31 | conn_process.start() 32 | 33 | 34 | class MyAppConfig(AppConfig): 35 | name = "apps.startup" 36 | verbose_name = "start tmtracker" 37 | 38 | def ready(self): 39 | run() 40 | -------------------------------------------------------------------------------- /apps/startup/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jackiebibili/ticket_tracker_api/d4f8451d49d6de4d9d7e1532b2496961e19c08db/apps/startup/migrations/__init__.py -------------------------------------------------------------------------------- /apps/startup/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /apps/startup/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/startup/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /apps/storage/base.py: -------------------------------------------------------------------------------- 1 | from pymongo import collection 2 | from uuid import uuid4 3 | from datetime import datetime 4 | 5 | def insert_one__(coll: collection.Collection, doc: dict): 6 | return coll.insert_one(doc) 7 | 8 | def insert_many__(coll: collection.Collection, docs: list): 9 | return coll.insert_many(docs) 10 | 11 | def delete_one__(coll: collection.Collection, filter: dict): 12 | return coll.delete_one(filter) 13 | 14 | def delete_many__(coll: collection.Collection, filter: dict): 15 | return coll.delete_many(filter) 16 | 17 | def find_one_and_replace__(coll: collection.Collection, filter: dict, new_doc: dict): 18 | new_doc.update(gen_additional_attrs()) 19 | return coll.find_one_and_replace(filter, new_doc) 20 | 21 | def find_one_and_update__(coll: collection.Collection, filter: dict, update=dict): 22 | meta_attrs = get_meta_attrs() 23 | key = "$set" 24 | if key in update: 25 | update[key].update(meta_attrs) 26 | else: 27 | update[key] = meta_attrs 28 | return coll.find_one_and_update(filter, update) 29 | 30 | def find_one_and_delete__(coll: collection.Collection, filter: dict): 31 | return coll.find_one_and_delete(filter) 32 | 33 | def find_one__(coll: collection.Collection, filter: dict, projection, **kwargs): 34 | return coll.find_one(filter=filter, projection=projection, **kwargs) 35 | 36 | def find_many__(coll: collection.Collection, filter: dict, projection, **kwargs): 37 | return coll.find(filter=filter, projection=projection, **kwargs) 38 | 39 | def count_docs__(coll: collection.Collection, filter: dict): 40 | return coll.count_documents(filter=filter) 41 | 42 | def estimated_count_docs__(coll: collection.Collection): 43 | return coll.estimated_document_count() 44 | 45 | def watch__(coll: collection.Collection, **kwargs): 46 | return coll.watch(**kwargs) 47 | 48 | def gen_additional_attrs(): 49 | return { 50 | "_id": str(uuid4()), 51 | **get_meta_attrs() 52 | } 53 | 54 | def get_meta_attrs(): 55 | return { 56 | "last_modified": datetime.today().replace(microsecond=0) 57 | } -------------------------------------------------------------------------------- /apps/storage/query.py: -------------------------------------------------------------------------------- 1 | from .storage import * 2 | import pymongo 3 | 4 | # find the max value in a collection 5 | def find_max(collection_name, filter: dict, sort_key: str, db_name="tickets"): 6 | sort_seq = [(sort_key, pymongo.DESCENDING)] 7 | return find_one(collection_name, filter, db_name=db_name, sort=sort_seq) 8 | 9 | # find the min value in a collection 10 | def find_min(collection_name, filter: dict, sort_key: str, db_name="tickets"): 11 | sort_seq = [(sort_key, pymongo.ASCENDING)] 12 | return find_one(collection_name, filter, db_name=db_name, sort=sort_seq) 13 | 14 | def find_many_ascending_order(collection_name, filter: dict, sort_key: str, db_name="tickets"): 15 | sort_seq = [(sort_key, pymongo.ASCENDING)] 16 | return find_many(collection_name, filter, db_name=db_name, sort=sort_seq) 17 | -------------------------------------------------------------------------------- /apps/storage/storage.py: -------------------------------------------------------------------------------- 1 | from utils import get_db_handle 2 | from .base import * 3 | 4 | # insert one 5 | def insert_one(collection_name, doc: dict, db_name="tickets"): 6 | db = get_db_handle(db_name) 7 | coll = db[collection_name] 8 | # additional attributes 9 | doc.update(gen_additional_attrs()) 10 | return insert_one__(coll, doc) 11 | 12 | # insert many 13 | def insert_many(collection_name, docs: list[dict], db_name="tickets"): 14 | if len(docs) == 0: 15 | return True 16 | db = get_db_handle(db_name) 17 | coll = db[collection_name] 18 | # additional attributes 19 | for doc in docs: 20 | doc.update(gen_additional_attrs()) 21 | return insert_many__(coll, docs) 22 | 23 | # delete one 24 | def delete_one(collection_name, filter: dict, db_name="tickets"): 25 | db = get_db_handle(db_name) 26 | coll = db[collection_name] 27 | return delete_one__(coll, filter) 28 | 29 | # delete many 30 | def delete_many(collection_name, filter: dict, db_name="tickets"): 31 | db = get_db_handle(db_name) 32 | coll = db[collection_name] 33 | return delete_many__(coll, filter) 34 | 35 | # find one and replace 36 | def find_one_and_replace(collection_name, filter: dict, new_doc: dict, db_name="tickets"): 37 | db = get_db_handle(db_name) 38 | coll = db[collection_name] 39 | return find_one_and_replace__(coll, filter, new_doc) 40 | 41 | # find one and update 42 | def find_one_and_update(collection_name, filter: dict, update, db_name="tickets"): 43 | db = get_db_handle(db_name) 44 | coll = db[collection_name] 45 | return find_one_and_update__(coll, filter, update) 46 | 47 | # find one and delete 48 | def find_one_and_delete(collection_name, filter: dict, db_name="tickets"): 49 | db = get_db_handle(db_name) 50 | coll = db[collection_name] 51 | return find_one_and_delete__(coll, filter) 52 | 53 | # find one 54 | def find_one(collection_name, filter: dict, projection=None, db_name="tickets", **kwargs): 55 | db = get_db_handle(db_name) 56 | coll = db[collection_name] 57 | return find_one__(coll, filter, projection, **kwargs) 58 | 59 | # find many 60 | def find_many(collection_name, filter: dict, projection=None, db_name="tickets", **kwargs): 61 | db = get_db_handle(db_name) 62 | coll = db[collection_name] 63 | return list(find_many__(coll, filter, projection, **kwargs)) 64 | 65 | # count with filter 66 | def count_docs(collection_name, filter: dict, db_name="tickets"): 67 | db = get_db_handle(db_name) 68 | coll = db[collection_name] 69 | return count_docs__(coll, filter) 70 | 71 | # count all docs in a collection 72 | def estimated_count_docs(collection_name, db_name="tickets"): 73 | db = get_db_handle(db_name) 74 | coll = db[collection_name] 75 | return estimated_count_docs__(coll) 76 | 77 | # watch changes 78 | def watch(collection_name, db_name="tickets", **kwargs): 79 | db = get_db_handle(db_name) 80 | coll = db[collection_name] 81 | return watch__(coll, **kwargs) 82 | 83 | -------------------------------------------------------------------------------- /apps/ticketscraping/README.md: -------------------------------------------------------------------------------- 1 | # TM QuickPicks 4.0 API Return Data Schema 2 | 3 | ``` 4 | { 5 | "meta": { 6 | "modified": ISOString, 7 | "expired": ISOString 8 | }, 9 | "eventId": string, 10 | "offset": number, 11 | "total": number, 12 | "picks": { 13 | "type": "seat", 14 | "selection": "resale" or "standard", 15 | "quality": float < 1, 16 | "section": string, 17 | "row": string of number, 18 | "offerGroups": [ 19 | { 20 | "offers": [ 21 | offerIds in string 22 | ], 23 | "places": [ 24 | placeIds in string 25 | ], 26 | "seats": [ 27 | seatIds in string 28 | ], 29 | "coordinates": [] 30 | } 31 | ], 32 | "area": string, 33 | "descriptionId": string, 34 | "maxQuantity": number of seats, 35 | "shapes": unknown, 36 | }[], 37 | 38 | "places": {}[], //ToDo 39 | 40 | "_embedded": { 41 | "offer": { 42 | "meta": { 43 | "modified": ISOString, 44 | "expires": ISOString 45 | }, 46 | "offerId": string, 47 | "rank": 0, 48 | "online": bool, 49 | "protected": bool, 50 | "rollup": bool, 51 | "inventoryType": "resale" or "primary", 52 | "offerType": "standard", 53 | "listingId": string, 54 | "listingVersionId": string, 55 | "currency": string, 56 | "listPrice": number, 57 | "faceValue": number, 58 | "totalPrice": number, 59 | "noChargesPrice": number, 60 | "charges": [ 61 | { 62 | "reason": "service", 63 | "type": "fee", 64 | "amount": number 65 | } 66 | ], 67 | "sellableQuantities": [ 68 | numbers 69 | ], 70 | "section": string, 71 | "row": string, 72 | "seatFrom": string, 73 | "seatTo": string, 74 | "ticketTypeId": string, 75 | }[] 76 | } 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /apps/ticketscraping/connection/asyn_tasks_receiver.py: -------------------------------------------------------------------------------- 1 | # start sockets 2 | from threading import Thread 3 | from multiprocessing import Process 4 | from apps.ticketscraping.connection.receiver_process import ReceiverProcess 5 | from apps.ticketscraping.constants import SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT 6 | 7 | 8 | def run_prepare(): 9 | # start receiver socket 10 | from apps.ticketscraping.connection.mail_receiver import run 11 | conn_process = Process(target=run, daemon=True) 12 | conn_process.start() 13 | 14 | # start sender socket 15 | from apps.ticketscraping.schedulers.mail_scheduler import mail_scheduler 16 | conn_thread = Thread(target=mail_scheduler.connect) 17 | conn_thread.start() 18 | # wait for mailer to connect 19 | conn_thread.join() 20 | 21 | # start itself 22 | from apps.ticketscraping.tasks.asynchronous import run_async_tasks 23 | receiver = ReceiverProcess(run_async_tasks, SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT) 24 | receiver.connect() 25 | receiver.serve_forever() 26 | 27 | 28 | def run(): 29 | # starter 30 | p = Process(target=run_prepare) 31 | p.start() 32 | 33 | 34 | if __name__ == '__main__': 35 | run() 36 | -------------------------------------------------------------------------------- /apps/ticketscraping/connection/mail_receiver.py: -------------------------------------------------------------------------------- 1 | from apps.ticketscraping.connection.receiver_process import ReceiverProcess 2 | from apps.pushnotification.smtp import send_email, auth_server 3 | from apps.ticketscraping.constants import SERVICE_LOCALHOST, MAIL_RECEIVER_PORT 4 | 5 | def run(): 6 | # start itself 7 | auth_server() 8 | receiver = ReceiverProcess(send_email, SERVICE_LOCALHOST, MAIL_RECEIVER_PORT) 9 | receiver.connect() 10 | receiver.serve_forever() 11 | 12 | if __name__ == '__main__': 13 | run() 14 | -------------------------------------------------------------------------------- /apps/ticketscraping/connection/receiver.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.connection import Client 2 | from threading import Semaphore 3 | 4 | class Receiver: 5 | def __init__(self, hostname: str, port: int): 6 | self.lock = Semaphore(1) 7 | self.hostname = hostname 8 | self.port = port 9 | self.conn = None 10 | 11 | def connect(self): 12 | self.conn = Client(address=(self.hostname, self.port,)) 13 | 14 | def recv(self): 15 | if self.conn is None: 16 | raise Exception('connection is not established') 17 | self.lock.acquire() 18 | res = self.conn.recv() 19 | self.lock.release() 20 | return res 21 | 22 | def __del__(self): 23 | if self.conn is not None: self.conn.close() 24 | 25 | -------------------------------------------------------------------------------- /apps/ticketscraping/connection/receiver_process.py: -------------------------------------------------------------------------------- 1 | from .receiver import Receiver 2 | 3 | class ReceiverProcess(Receiver): 4 | def __init__(self, action, hostname: str, port: int): 5 | super().__init__(hostname, port) 6 | self.action = action 7 | 8 | def serve_forever(self): 9 | while True: 10 | res = self.recv() 11 | self.action(*res) 12 | -------------------------------------------------------------------------------- /apps/ticketscraping/connection/sender.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.connection import Listener 2 | from threading import Semaphore 3 | 4 | class Sender: 5 | def __init__(self, hostname: str, port: int): 6 | self.lock = Semaphore(1) 7 | self.hostname = hostname 8 | self.port = port 9 | self.conn = None 10 | 11 | def connect(self): 12 | listener = Listener(address=(self.hostname, self.port)) 13 | self.conn = listener.accept() 14 | print("conn accepted ", self.port) 15 | 16 | def send(self, *args): 17 | if self.conn is None: 18 | raise Exception('connection is not established') 19 | self.lock.acquire() 20 | self.conn.send(args) 21 | self.lock.release() 22 | return True 23 | 24 | def __del__(self): 25 | if self.conn is not None: 26 | self.conn.close() 27 | -------------------------------------------------------------------------------- /apps/ticketscraping/constants.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | # services - async action handlers 4 | ASYNC_TASKS_RECEIVER_PORT = 8100 5 | MAIL_RECEIVER_PORT = 8200 6 | SERVICE_LOCALHOST = 'localhost' 7 | 8 | ANTIBOT_JS_CODE_URL = "https://epsf.ticketmaster.com/eps-d" 9 | TOKEN_INTERROGATION_URL = "https://epsf.ticketmaster.com/eps-d?d=www.ticketmaster.com" 10 | 11 | 12 | def get_top_picks_url( 13 | eventId): return f"https://offeradapter.ticketmaster.com/api/ismds/event/{eventId}/quickpicks" 14 | 15 | 16 | EVENT_ID = "0C005B5587A017CF" 17 | BASIC_REQ_HEADER = {"origin": "https://www.ticketmaster.com", 18 | "referer": "https://www.ticketmaster.com/"} 19 | DATABASE = { 20 | "EVENTS": "events", 21 | "TOP_PICKS": "top-picks", 22 | "BEST_AVAILABLE_SEATS": "best-available-seats", 23 | "BEST_HISTORY_SEATS": "best-history-seats" 24 | } 25 | 26 | SUBSCRIBE_REQUEST_PROPS = { 27 | 'NAME': 'name', 28 | 'CLIENT_NAME': 'client_name', 29 | 'CLIENT_EMAILS': 'client_emails', 30 | 'TARGET_PRICE': 'target_price', 31 | 'TOLERANCE': 'tolerance', 32 | 'TICKET_NUM': 'ticket_num', 33 | 'TM_EVENT_ID': 'tm_event_id' 34 | } 35 | 36 | def filter_obj_from_attrs(obj, atts: dict[str,str]): 37 | res = {} 38 | for key in atts.values(): 39 | if key in obj: 40 | res[key] = obj[key] 41 | if len(res) != len(atts): 42 | raise Exception('lack of attributes') 43 | return res 44 | 45 | 46 | # metric thresholds 47 | MINIMUM_HISTORY_DATA = 3 48 | PERCENT_OF_CHANGE = 0.5 49 | PERCENTILE_HISTORY_PRICES = 0.25 50 | 51 | # alert content constants 52 | ALERT_SEATS_MAX_COUNT = 3 53 | TOP_COMPARED_HISTORY_SEATS = 3 54 | 55 | def get_top_picks_header(): return { 56 | **BASIC_REQ_HEADER, 57 | "tmps-correlation-id": str(uuid4()) 58 | } 59 | 60 | def get_top_picks_query_params(qty: int, target_price: int, tolerance: int): return { 61 | 'show': 'places maxQuantity sections', 62 | 'mode': 'primary:ppsectionrow resale:ga_areas platinum:all', 63 | 'qty': qty, 64 | 'q': f"and(not(\'accessible\'),any(listprices,$and(gte(@,{target_price - tolerance}),lte(@,{target_price + tolerance}))))", 65 | 'includeStandard': 'true', 66 | 'includeResale': 'true', 67 | 'includePlatinumInventoryType': 'false', 68 | 'embed': ['area', 'offer', 'description'], 69 | 'apikey': 'b462oi7fic6pehcdkzony5bxhe', 70 | 'apisecret': 'pquzpfrfz7zd2ylvtz3w5dtyse', 71 | 'limit': 100, 72 | 'offset': 0, 73 | 'sort': '-quality', 74 | } 75 | 76 | FN_MATCHING_REGEX = r"\(function\(\){.*}\)\(\)" 77 | TOKEN_RENEW_SEC_OFFSET = 3 78 | TOKEN_RENEW_PRIORITY = 1 79 | TICKET_SCRAPING_PRIORITY = 3 80 | TICKET_SCRAPING_INTERVAL = 60 81 | TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL = 10 82 | 83 | INJECTOR_LOCATION = "js/injector.js" 84 | INJECTOR_HEADER_LOCATION = "js/injector-header.js" 85 | RENNABLE_FILENAME = "js/antibot-simulation.js" 86 | -------------------------------------------------------------------------------- /apps/ticketscraping/js/injector-header.js: -------------------------------------------------------------------------------- 1 | const jsdom = require("jsdom"); 2 | const { JSDOM } = jsdom; 3 | const { window } = new JSDOM(""); 4 | -------------------------------------------------------------------------------- /apps/ticketscraping/js/injector.js: -------------------------------------------------------------------------------- 1 | /* THE CODE BELOW SHOULD BE INJECTED TO THE JS FILE 2 | AT https://epsf.ticketmaster.com/eps-d 3 | IN ORDER TO OBTAIN A REESE84 TOKEN FROM THE SERVER */ 4 | 5 | /** 6 | * Reference: https://epsf.ticketmaster.com/eps-d 7 | */ 8 | const reese84HashObj = { 9 | hash: function (_0x32cda0) { 10 | _0x32cda0 = unescape(encodeURIComponent(_0x32cda0)); 11 | for ( 12 | var _0x354dff = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6], 13 | _0x184815 = 14 | (_0x32cda0 += String['fromCharCode'](0x80))['length'] / 0x4 + 0x2, 15 | _0x4f0814 = Math['ceil'](_0x184815 / 0x10), 16 | _0xb67cd8 = new Array(_0x4f0814), 17 | _0x3db23f = 0x0; 18 | _0x3db23f < _0x4f0814; 19 | _0x3db23f++ 20 | ) { 21 | _0xb67cd8[_0x3db23f] = new Array(0x10); 22 | for (var _0x4d1d12 = 0x0; _0x4d1d12 < 0x10; _0x4d1d12++) 23 | _0xb67cd8[_0x3db23f][_0x4d1d12] = 24 | (_0x32cda0['charCodeAt'](0x40 * _0x3db23f + 0x4 * _0x4d1d12) << 25 | 0x18) | 26 | (_0x32cda0['charCodeAt'](0x40 * _0x3db23f + 0x4 * _0x4d1d12 + 0x1) << 27 | 0x10) | 28 | (_0x32cda0['charCodeAt'](0x40 * _0x3db23f + 0x4 * _0x4d1d12 + 0x2) << 29 | 0x8) | 30 | _0x32cda0['charCodeAt'](0x40 * _0x3db23f + 0x4 * _0x4d1d12 + 0x3); 31 | } 32 | (_0xb67cd8[_0x4f0814 - 0x1][0xe] = 33 | (0x8 * (_0x32cda0['length'] - 0x1)) / Math['pow'](0x2, 0x20)), 34 | (_0xb67cd8[_0x4f0814 - 0x1][0xe] = Math['floor']( 35 | _0xb67cd8[_0x4f0814 - 0x1][0xe] 36 | )), 37 | (_0xb67cd8[_0x4f0814 - 0x1][0xf] = 38 | (0x8 * (_0x32cda0['length'] - 0x1)) & 0xffffffff); 39 | var _0x4964ee, 40 | _0x1f8408, 41 | _0x393864, 42 | _0x4e34f7, 43 | _0x2bdc40, 44 | _0x725508 = 0x67452301, 45 | _0x69fa2d = 0xefcdab89, 46 | _0xc2205d = 0x98badcfe, 47 | _0x4d6fc6 = 0x10325476, 48 | _0x531366 = 0xc3d2e1f0, 49 | _0xe1407f = new Array(0x50); 50 | for (_0x3db23f = 0x0; _0x3db23f < _0x4f0814; _0x3db23f++) { 51 | for (var _0x19ef55 = 0x0; _0x19ef55 < 0x10; _0x19ef55++) 52 | _0xe1407f[_0x19ef55] = _0xb67cd8[_0x3db23f][_0x19ef55]; 53 | for (_0x19ef55 = 0x10; _0x19ef55 < 0x50; _0x19ef55++) 54 | _0xe1407f[_0x19ef55] = reese84HashObj['ROTL']( 55 | _0xe1407f[_0x19ef55 - 0x3] ^ 56 | _0xe1407f[_0x19ef55 - 0x8] ^ 57 | _0xe1407f[_0x19ef55 - 0xe] ^ 58 | _0xe1407f[_0x19ef55 - 0x10], 59 | 0x1 60 | ); 61 | (_0x4964ee = _0x725508), 62 | (_0x1f8408 = _0x69fa2d), 63 | (_0x393864 = _0xc2205d), 64 | (_0x4e34f7 = _0x4d6fc6), 65 | (_0x2bdc40 = _0x531366); 66 | for (_0x19ef55 = 0x0; _0x19ef55 < 0x50; _0x19ef55++) { 67 | var _0x1f693f = Math['floor'](_0x19ef55 / 0x14), 68 | _0x1e561e = 69 | (reese84HashObj['ROTL'](_0x4964ee, 0x5) + 70 | reese84HashObj['f'](_0x1f693f, _0x1f8408, _0x393864, _0x4e34f7) + 71 | _0x2bdc40 + 72 | _0x354dff[_0x1f693f] + 73 | _0xe1407f[_0x19ef55]) & 74 | 0xffffffff; 75 | (_0x2bdc40 = _0x4e34f7), 76 | (_0x4e34f7 = _0x393864), 77 | (_0x393864 = reese84HashObj['ROTL'](_0x1f8408, 0x1e)), 78 | (_0x1f8408 = _0x4964ee), 79 | (_0x4964ee = _0x1e561e); 80 | } 81 | (_0x725508 = (_0x725508 + _0x4964ee) & 0xffffffff), 82 | (_0x69fa2d = (_0x69fa2d + _0x1f8408) & 0xffffffff), 83 | (_0xc2205d = (_0xc2205d + _0x393864) & 0xffffffff), 84 | (_0x4d6fc6 = (_0x4d6fc6 + _0x4e34f7) & 0xffffffff), 85 | (_0x531366 = (_0x531366 + _0x2bdc40) & 0xffffffff); 86 | } 87 | return ( 88 | reese84HashObj['toHexStr'](_0x725508) + 89 | reese84HashObj['toHexStr'](_0x69fa2d) + 90 | reese84HashObj['toHexStr'](_0xc2205d) + 91 | reese84HashObj['toHexStr'](_0x4d6fc6) + 92 | reese84HashObj['toHexStr'](_0x531366) 93 | ); 94 | }, 95 | f: function (_0x4a2ab7, _0x15ec48, _0x2f0b8e, _0x6ffd09) { 96 | switch (_0x4a2ab7) { 97 | case 0x0: 98 | return (_0x15ec48 & _0x2f0b8e) ^ (~_0x15ec48 & _0x6ffd09); 99 | case 0x1: 100 | case 0x3: 101 | return _0x15ec48 ^ _0x2f0b8e ^ _0x6ffd09; 102 | case 0x2: 103 | return ( 104 | (_0x15ec48 & _0x2f0b8e) ^ 105 | (_0x15ec48 & _0x6ffd09) ^ 106 | (_0x2f0b8e & _0x6ffd09) 107 | ); 108 | } 109 | }, 110 | ROTL: function (_0x11e783, _0x3f2f62) { 111 | return (_0x11e783 << _0x3f2f62) | (_0x11e783 >>> (0x20 - _0x3f2f62)); 112 | }, 113 | toHexStr: function (_0x125300) { 114 | for (var _0x39e29d = '', _0xa51676 = 0x7; _0xa51676 >= 0x0; _0xa51676--) 115 | _0x39e29d += ((_0x125300 >>> (0x4 * _0xa51676)) & 0xf)['toString'](0x10); 116 | return _0x39e29d; 117 | }, 118 | }; 119 | 120 | /** 121 | * Reference: https://epsf.ticketmaster.com/eps-d 122 | */ 123 | function _0xbae375() { 124 | return Date['now'] ? Date['now']() : new Date()['getTime'](); 125 | } 126 | 127 | /** 128 | * Reference: https://epsf.ticketmaster.com/eps-d 129 | */ 130 | const timerFactory = (function () { 131 | /** 132 | * 133 | * start: ƒ (_0x3deed1) 134 | startInternal: ƒ (_0xd257e7) 135 | stop: ƒ (_0x975271) 136 | stopInternal: ƒ (_0x2f5e67) 137 | summary: ƒ () 138 | */ 139 | function _0x1efe8f() { 140 | (this['marks'] = {}), (this['measures'] = {}); 141 | } 142 | return ( 143 | (_0x1efe8f['prototype']['start'] = function (_0x3deed1) { 144 | this['marks'][_0x3deed1] = _0xbae375(); 145 | }), 146 | (_0x1efe8f['prototype']['startInternal'] = function (_0xd257e7) {}), 147 | (_0x1efe8f['prototype']['stop'] = function (_0x975271) { 148 | this['measures'][_0x975271] = _0xbae375() - this['marks'][_0x975271]; 149 | }), 150 | (_0x1efe8f['prototype']['stopInternal'] = function (_0x2f5e67) {}), 151 | (_0x1efe8f['prototype']['summary'] = function () { 152 | return this['measures']; 153 | }), 154 | _0x1efe8f 155 | ); 156 | })(); 157 | 158 | /* injector code starts here */ 159 | 160 | // const fs = require('fs'); 161 | // const FILE_NAME = "reese84.json"; 162 | 163 | function writeResultToFile(data) { 164 | process.stdout.write(JSON.stringify(data)) 165 | } 166 | 167 | async function getInterrogation() { 168 | const INTERROGATOR = 'reese84interrogator', 169 | INTERROGATION = 'interrogate'; 170 | const interrogationFn = window[INTERROGATOR]; 171 | if (!interrogationFn) { 172 | throw new Error('Interrogation function not extracted'); 173 | } 174 | let fn = {}; 175 | const timer = new timerFactory(); 176 | timer.start('total'); 177 | 178 | interrogationFn.call(fn, reese84HashObj.hash, timer); 179 | const res = await new Promise(fn[INTERROGATION]); 180 | return [res, timer]; 181 | }; 182 | 183 | 184 | /** 185 | * main function 186 | */ 187 | (async function () { 188 | const [reese84, timer] = await getInterrogation(); 189 | const body = { 190 | error: null, 191 | old_token: null, 192 | performance: { interrogation: timer.measures.interrogation }, 193 | solution: { interrogation: reese84 }, 194 | }; 195 | writeResultToFile(body); 196 | })(); 197 | -------------------------------------------------------------------------------- /apps/ticketscraping/js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdom-env", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@mapbox/node-pre-gyp": { 8 | "version": "1.0.9", 9 | "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", 10 | "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", 11 | "requires": { 12 | "detect-libc": "^2.0.0", 13 | "https-proxy-agent": "^5.0.0", 14 | "make-dir": "^3.1.0", 15 | "node-fetch": "^2.6.7", 16 | "nopt": "^5.0.0", 17 | "npmlog": "^5.0.1", 18 | "rimraf": "^3.0.2", 19 | "semver": "^7.3.5", 20 | "tar": "^6.1.11" 21 | } 22 | }, 23 | "@tootallnate/once": { 24 | "version": "2.0.0", 25 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", 26 | "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" 27 | }, 28 | "abab": { 29 | "version": "2.0.6", 30 | "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", 31 | "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" 32 | }, 33 | "abbrev": { 34 | "version": "1.1.1", 35 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 36 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 37 | }, 38 | "acorn": { 39 | "version": "8.7.1", 40 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", 41 | "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==" 42 | }, 43 | "acorn-globals": { 44 | "version": "6.0.0", 45 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", 46 | "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", 47 | "requires": { 48 | "acorn": "^7.1.1", 49 | "acorn-walk": "^7.1.1" 50 | }, 51 | "dependencies": { 52 | "acorn": { 53 | "version": "7.4.1", 54 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", 55 | "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" 56 | } 57 | } 58 | }, 59 | "acorn-walk": { 60 | "version": "7.2.0", 61 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", 62 | "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" 63 | }, 64 | "agent-base": { 65 | "version": "6.0.2", 66 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 67 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 68 | "requires": { 69 | "debug": "4" 70 | } 71 | }, 72 | "ansi-regex": { 73 | "version": "5.0.1", 74 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 75 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 76 | }, 77 | "aproba": { 78 | "version": "2.0.0", 79 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", 80 | "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" 81 | }, 82 | "are-we-there-yet": { 83 | "version": "2.0.0", 84 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", 85 | "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", 86 | "requires": { 87 | "delegates": "^1.0.0", 88 | "readable-stream": "^3.6.0" 89 | } 90 | }, 91 | "asynckit": { 92 | "version": "0.4.0", 93 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 94 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 95 | }, 96 | "balanced-match": { 97 | "version": "1.0.2", 98 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 99 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 100 | }, 101 | "brace-expansion": { 102 | "version": "1.1.11", 103 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 104 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 105 | "requires": { 106 | "balanced-match": "^1.0.0", 107 | "concat-map": "0.0.1" 108 | } 109 | }, 110 | "browser-process-hrtime": { 111 | "version": "1.0.0", 112 | "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", 113 | "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" 114 | }, 115 | "canvas": { 116 | "version": "2.9.1", 117 | "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz", 118 | "integrity": "sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==", 119 | "requires": { 120 | "@mapbox/node-pre-gyp": "^1.0.0", 121 | "nan": "^2.15.0", 122 | "simple-get": "^3.0.3" 123 | } 124 | }, 125 | "chownr": { 126 | "version": "2.0.0", 127 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", 128 | "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" 129 | }, 130 | "color-support": { 131 | "version": "1.1.3", 132 | "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", 133 | "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" 134 | }, 135 | "combined-stream": { 136 | "version": "1.0.8", 137 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 138 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 139 | "requires": { 140 | "delayed-stream": "~1.0.0" 141 | } 142 | }, 143 | "concat-map": { 144 | "version": "0.0.1", 145 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 146 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 147 | }, 148 | "console-control-strings": { 149 | "version": "1.1.0", 150 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 151 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" 152 | }, 153 | "cssom": { 154 | "version": "0.5.0", 155 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 156 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" 157 | }, 158 | "cssstyle": { 159 | "version": "2.3.0", 160 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", 161 | "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", 162 | "requires": { 163 | "cssom": "~0.3.6" 164 | }, 165 | "dependencies": { 166 | "cssom": { 167 | "version": "0.3.8", 168 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", 169 | "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" 170 | } 171 | } 172 | }, 173 | "data-urls": { 174 | "version": "3.0.2", 175 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", 176 | "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", 177 | "requires": { 178 | "abab": "^2.0.6", 179 | "whatwg-mimetype": "^3.0.0", 180 | "whatwg-url": "^11.0.0" 181 | }, 182 | "dependencies": { 183 | "whatwg-url": { 184 | "version": "11.0.0", 185 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", 186 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", 187 | "requires": { 188 | "tr46": "^3.0.0", 189 | "webidl-conversions": "^7.0.0" 190 | } 191 | } 192 | } 193 | }, 194 | "debug": { 195 | "version": "4.3.4", 196 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 197 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 198 | "requires": { 199 | "ms": "2.1.2" 200 | } 201 | }, 202 | "decimal.js": { 203 | "version": "10.3.1", 204 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", 205 | "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" 206 | }, 207 | "decompress-response": { 208 | "version": "4.2.1", 209 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 210 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 211 | "requires": { 212 | "mimic-response": "^2.0.0" 213 | } 214 | }, 215 | "deep-is": { 216 | "version": "0.1.4", 217 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 218 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" 219 | }, 220 | "delayed-stream": { 221 | "version": "1.0.0", 222 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 223 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 224 | }, 225 | "delegates": { 226 | "version": "1.0.0", 227 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 228 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" 229 | }, 230 | "detect-libc": { 231 | "version": "2.0.1", 232 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", 233 | "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" 234 | }, 235 | "domexception": { 236 | "version": "4.0.0", 237 | "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", 238 | "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", 239 | "requires": { 240 | "webidl-conversions": "^7.0.0" 241 | } 242 | }, 243 | "emoji-regex": { 244 | "version": "8.0.0", 245 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 246 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 247 | }, 248 | "escodegen": { 249 | "version": "2.0.0", 250 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", 251 | "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", 252 | "requires": { 253 | "esprima": "^4.0.1", 254 | "estraverse": "^5.2.0", 255 | "esutils": "^2.0.2", 256 | "optionator": "^0.8.1", 257 | "source-map": "~0.6.1" 258 | } 259 | }, 260 | "esprima": { 261 | "version": "4.0.1", 262 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 263 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 264 | }, 265 | "estraverse": { 266 | "version": "5.3.0", 267 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 268 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" 269 | }, 270 | "esutils": { 271 | "version": "2.0.3", 272 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 273 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" 274 | }, 275 | "fast-levenshtein": { 276 | "version": "2.0.6", 277 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 278 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" 279 | }, 280 | "form-data": { 281 | "version": "4.0.0", 282 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 283 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 284 | "requires": { 285 | "asynckit": "^0.4.0", 286 | "combined-stream": "^1.0.8", 287 | "mime-types": "^2.1.12" 288 | } 289 | }, 290 | "fs-minipass": { 291 | "version": "2.1.0", 292 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", 293 | "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", 294 | "requires": { 295 | "minipass": "^3.0.0" 296 | } 297 | }, 298 | "fs.realpath": { 299 | "version": "1.0.0", 300 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 301 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 302 | }, 303 | "gauge": { 304 | "version": "3.0.2", 305 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", 306 | "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", 307 | "requires": { 308 | "aproba": "^1.0.3 || ^2.0.0", 309 | "color-support": "^1.1.2", 310 | "console-control-strings": "^1.0.0", 311 | "has-unicode": "^2.0.1", 312 | "object-assign": "^4.1.1", 313 | "signal-exit": "^3.0.0", 314 | "string-width": "^4.2.3", 315 | "strip-ansi": "^6.0.1", 316 | "wide-align": "^1.1.2" 317 | } 318 | }, 319 | "glob": { 320 | "version": "7.2.3", 321 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 322 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 323 | "requires": { 324 | "fs.realpath": "^1.0.0", 325 | "inflight": "^1.0.4", 326 | "inherits": "2", 327 | "minimatch": "^3.1.1", 328 | "once": "^1.3.0", 329 | "path-is-absolute": "^1.0.0" 330 | } 331 | }, 332 | "has-unicode": { 333 | "version": "2.0.1", 334 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 335 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" 336 | }, 337 | "html-encoding-sniffer": { 338 | "version": "3.0.0", 339 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 340 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 341 | "requires": { 342 | "whatwg-encoding": "^2.0.0" 343 | } 344 | }, 345 | "http-proxy-agent": { 346 | "version": "5.0.0", 347 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", 348 | "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", 349 | "requires": { 350 | "@tootallnate/once": "2", 351 | "agent-base": "6", 352 | "debug": "4" 353 | } 354 | }, 355 | "https-proxy-agent": { 356 | "version": "5.0.1", 357 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 358 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 359 | "requires": { 360 | "agent-base": "6", 361 | "debug": "4" 362 | } 363 | }, 364 | "iconv-lite": { 365 | "version": "0.6.3", 366 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 367 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 368 | "requires": { 369 | "safer-buffer": ">= 2.1.2 < 3.0.0" 370 | } 371 | }, 372 | "inflight": { 373 | "version": "1.0.6", 374 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 375 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 376 | "requires": { 377 | "once": "^1.3.0", 378 | "wrappy": "1" 379 | } 380 | }, 381 | "inherits": { 382 | "version": "2.0.4", 383 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 384 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 385 | }, 386 | "is-fullwidth-code-point": { 387 | "version": "3.0.0", 388 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 389 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 390 | }, 391 | "is-potential-custom-element-name": { 392 | "version": "1.0.1", 393 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", 394 | "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" 395 | }, 396 | "jsdom": { 397 | "version": "19.0.0", 398 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", 399 | "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==", 400 | "requires": { 401 | "abab": "^2.0.5", 402 | "acorn": "^8.5.0", 403 | "acorn-globals": "^6.0.0", 404 | "cssom": "^0.5.0", 405 | "cssstyle": "^2.3.0", 406 | "data-urls": "^3.0.1", 407 | "decimal.js": "^10.3.1", 408 | "domexception": "^4.0.0", 409 | "escodegen": "^2.0.0", 410 | "form-data": "^4.0.0", 411 | "html-encoding-sniffer": "^3.0.0", 412 | "http-proxy-agent": "^5.0.0", 413 | "https-proxy-agent": "^5.0.0", 414 | "is-potential-custom-element-name": "^1.0.1", 415 | "nwsapi": "^2.2.0", 416 | "parse5": "6.0.1", 417 | "saxes": "^5.0.1", 418 | "symbol-tree": "^3.2.4", 419 | "tough-cookie": "^4.0.0", 420 | "w3c-hr-time": "^1.0.2", 421 | "w3c-xmlserializer": "^3.0.0", 422 | "webidl-conversions": "^7.0.0", 423 | "whatwg-encoding": "^2.0.0", 424 | "whatwg-mimetype": "^3.0.0", 425 | "whatwg-url": "^10.0.0", 426 | "ws": "^8.2.3", 427 | "xml-name-validator": "^4.0.0" 428 | } 429 | }, 430 | "levn": { 431 | "version": "0.3.0", 432 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 433 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 434 | "requires": { 435 | "prelude-ls": "~1.1.2", 436 | "type-check": "~0.3.2" 437 | } 438 | }, 439 | "lru-cache": { 440 | "version": "6.0.0", 441 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 442 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 443 | "requires": { 444 | "yallist": "^4.0.0" 445 | } 446 | }, 447 | "make-dir": { 448 | "version": "3.1.0", 449 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 450 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 451 | "requires": { 452 | "semver": "^6.0.0" 453 | }, 454 | "dependencies": { 455 | "semver": { 456 | "version": "6.3.0", 457 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 458 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" 459 | } 460 | } 461 | }, 462 | "mime-db": { 463 | "version": "1.52.0", 464 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 465 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 466 | }, 467 | "mime-types": { 468 | "version": "2.1.35", 469 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 470 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 471 | "requires": { 472 | "mime-db": "1.52.0" 473 | } 474 | }, 475 | "mimic-response": { 476 | "version": "2.1.0", 477 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 478 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 479 | }, 480 | "minimatch": { 481 | "version": "3.1.2", 482 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 483 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 484 | "requires": { 485 | "brace-expansion": "^1.1.7" 486 | } 487 | }, 488 | "minipass": { 489 | "version": "3.1.6", 490 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", 491 | "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", 492 | "requires": { 493 | "yallist": "^4.0.0" 494 | } 495 | }, 496 | "minizlib": { 497 | "version": "2.1.2", 498 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", 499 | "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", 500 | "requires": { 501 | "minipass": "^3.0.0", 502 | "yallist": "^4.0.0" 503 | } 504 | }, 505 | "mkdirp": { 506 | "version": "1.0.4", 507 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 508 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 509 | }, 510 | "ms": { 511 | "version": "2.1.2", 512 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 513 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 514 | }, 515 | "nan": { 516 | "version": "2.16.0", 517 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", 518 | "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==" 519 | }, 520 | "node-fetch": { 521 | "version": "2.6.7", 522 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 523 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 524 | "requires": { 525 | "whatwg-url": "^5.0.0" 526 | }, 527 | "dependencies": { 528 | "tr46": { 529 | "version": "0.0.3", 530 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 531 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 532 | }, 533 | "webidl-conversions": { 534 | "version": "3.0.1", 535 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 536 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 537 | }, 538 | "whatwg-url": { 539 | "version": "5.0.0", 540 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 541 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 542 | "requires": { 543 | "tr46": "~0.0.3", 544 | "webidl-conversions": "^3.0.0" 545 | } 546 | } 547 | } 548 | }, 549 | "nopt": { 550 | "version": "5.0.0", 551 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", 552 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", 553 | "requires": { 554 | "abbrev": "1" 555 | } 556 | }, 557 | "npmlog": { 558 | "version": "5.0.1", 559 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", 560 | "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", 561 | "requires": { 562 | "are-we-there-yet": "^2.0.0", 563 | "console-control-strings": "^1.1.0", 564 | "gauge": "^3.0.0", 565 | "set-blocking": "^2.0.0" 566 | } 567 | }, 568 | "nwsapi": { 569 | "version": "2.2.0", 570 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", 571 | "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" 572 | }, 573 | "object-assign": { 574 | "version": "4.1.1", 575 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 576 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 577 | }, 578 | "once": { 579 | "version": "1.4.0", 580 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 581 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 582 | "requires": { 583 | "wrappy": "1" 584 | } 585 | }, 586 | "optionator": { 587 | "version": "0.8.3", 588 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 589 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 590 | "requires": { 591 | "deep-is": "~0.1.3", 592 | "fast-levenshtein": "~2.0.6", 593 | "levn": "~0.3.0", 594 | "prelude-ls": "~1.1.2", 595 | "type-check": "~0.3.2", 596 | "word-wrap": "~1.2.3" 597 | } 598 | }, 599 | "parse5": { 600 | "version": "6.0.1", 601 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", 602 | "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" 603 | }, 604 | "path-is-absolute": { 605 | "version": "1.0.1", 606 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 607 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" 608 | }, 609 | "prelude-ls": { 610 | "version": "1.1.2", 611 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 612 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" 613 | }, 614 | "psl": { 615 | "version": "1.8.0", 616 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 617 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 618 | }, 619 | "punycode": { 620 | "version": "2.1.1", 621 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 622 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 623 | }, 624 | "readable-stream": { 625 | "version": "3.6.0", 626 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 627 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 628 | "requires": { 629 | "inherits": "^2.0.3", 630 | "string_decoder": "^1.1.1", 631 | "util-deprecate": "^1.0.1" 632 | } 633 | }, 634 | "rimraf": { 635 | "version": "3.0.2", 636 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 637 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 638 | "requires": { 639 | "glob": "^7.1.3" 640 | } 641 | }, 642 | "safe-buffer": { 643 | "version": "5.2.1", 644 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 645 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 646 | }, 647 | "safer-buffer": { 648 | "version": "2.1.2", 649 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 650 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 651 | }, 652 | "saxes": { 653 | "version": "5.0.1", 654 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", 655 | "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", 656 | "requires": { 657 | "xmlchars": "^2.2.0" 658 | } 659 | }, 660 | "semver": { 661 | "version": "7.3.7", 662 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", 663 | "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", 664 | "requires": { 665 | "lru-cache": "^6.0.0" 666 | } 667 | }, 668 | "set-blocking": { 669 | "version": "2.0.0", 670 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 671 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 672 | }, 673 | "signal-exit": { 674 | "version": "3.0.7", 675 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 676 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 677 | }, 678 | "simple-concat": { 679 | "version": "1.0.1", 680 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 681 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 682 | }, 683 | "simple-get": { 684 | "version": "3.1.1", 685 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", 686 | "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", 687 | "requires": { 688 | "decompress-response": "^4.2.0", 689 | "once": "^1.3.1", 690 | "simple-concat": "^1.0.0" 691 | } 692 | }, 693 | "source-map": { 694 | "version": "0.6.1", 695 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 696 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 697 | "optional": true 698 | }, 699 | "string-width": { 700 | "version": "4.2.3", 701 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 702 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 703 | "requires": { 704 | "emoji-regex": "^8.0.0", 705 | "is-fullwidth-code-point": "^3.0.0", 706 | "strip-ansi": "^6.0.1" 707 | } 708 | }, 709 | "string_decoder": { 710 | "version": "1.3.0", 711 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 712 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 713 | "requires": { 714 | "safe-buffer": "~5.2.0" 715 | } 716 | }, 717 | "strip-ansi": { 718 | "version": "6.0.1", 719 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 720 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 721 | "requires": { 722 | "ansi-regex": "^5.0.1" 723 | } 724 | }, 725 | "symbol-tree": { 726 | "version": "3.2.4", 727 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", 728 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" 729 | }, 730 | "tar": { 731 | "version": "6.1.11", 732 | "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", 733 | "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", 734 | "requires": { 735 | "chownr": "^2.0.0", 736 | "fs-minipass": "^2.0.0", 737 | "minipass": "^3.0.0", 738 | "minizlib": "^2.1.1", 739 | "mkdirp": "^1.0.3", 740 | "yallist": "^4.0.0" 741 | } 742 | }, 743 | "tough-cookie": { 744 | "version": "4.0.0", 745 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", 746 | "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", 747 | "requires": { 748 | "psl": "^1.1.33", 749 | "punycode": "^2.1.1", 750 | "universalify": "^0.1.2" 751 | } 752 | }, 753 | "tr46": { 754 | "version": "3.0.0", 755 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", 756 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", 757 | "requires": { 758 | "punycode": "^2.1.1" 759 | } 760 | }, 761 | "type-check": { 762 | "version": "0.3.2", 763 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 764 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 765 | "requires": { 766 | "prelude-ls": "~1.1.2" 767 | } 768 | }, 769 | "universalify": { 770 | "version": "0.1.2", 771 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 772 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" 773 | }, 774 | "util-deprecate": { 775 | "version": "1.0.2", 776 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 777 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 778 | }, 779 | "w3c-hr-time": { 780 | "version": "1.0.2", 781 | "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", 782 | "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", 783 | "requires": { 784 | "browser-process-hrtime": "^1.0.0" 785 | } 786 | }, 787 | "w3c-xmlserializer": { 788 | "version": "3.0.0", 789 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", 790 | "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", 791 | "requires": { 792 | "xml-name-validator": "^4.0.0" 793 | } 794 | }, 795 | "webidl-conversions": { 796 | "version": "7.0.0", 797 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 798 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" 799 | }, 800 | "whatwg-encoding": { 801 | "version": "2.0.0", 802 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 803 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 804 | "requires": { 805 | "iconv-lite": "0.6.3" 806 | } 807 | }, 808 | "whatwg-mimetype": { 809 | "version": "3.0.0", 810 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", 811 | "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" 812 | }, 813 | "whatwg-url": { 814 | "version": "10.0.0", 815 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", 816 | "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", 817 | "requires": { 818 | "tr46": "^3.0.0", 819 | "webidl-conversions": "^7.0.0" 820 | } 821 | }, 822 | "wide-align": { 823 | "version": "1.1.5", 824 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", 825 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", 826 | "requires": { 827 | "string-width": "^1.0.2 || 2 || 3 || 4" 828 | } 829 | }, 830 | "word-wrap": { 831 | "version": "1.2.3", 832 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 833 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" 834 | }, 835 | "wrappy": { 836 | "version": "1.0.2", 837 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 838 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 839 | }, 840 | "ws": { 841 | "version": "8.7.0", 842 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", 843 | "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==" 844 | }, 845 | "xml-name-validator": { 846 | "version": "4.0.0", 847 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", 848 | "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==" 849 | }, 850 | "xmlchars": { 851 | "version": "2.2.0", 852 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 853 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" 854 | }, 855 | "yallist": { 856 | "version": "4.0.0", 857 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 858 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 859 | } 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /apps/ticketscraping/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdom-env", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "antibot-simulation.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npm ci" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "canvas": "^2.9.1", 14 | "jsdom": "^19.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/ticketscraping/models/pick.py: -------------------------------------------------------------------------------- 1 | class Pick(): 2 | def __init__(self, type, selection, quality, section, row, area, maxQuantity, offer, seat_columns, _id=None, scraping_id=None): 3 | self._id = _id 4 | self.scraping_id = scraping_id 5 | self.type = type 6 | self.selection = selection 7 | self.quality = quality 8 | self.section = section 9 | self.row = row 10 | self.area = area 11 | self.maxQuantity = maxQuantity 12 | self.offer = offer 13 | self.price = offer.get('listPrice') 14 | self.seat_columns = seat_columns 15 | 16 | def setScrapingId(self, scraping_id: str): 17 | self.scraping_id = scraping_id 18 | 19 | def __eq__(self, other): 20 | return (self.section == other.section and self.row == other.row and 21 | ((type(self.seat_columns) is list and len( 22 | self.seat_columns) > 0 and type(other.seat_columns) is list and len( 23 | other.seat_columns) > 0 and self.seat_columns[0] == other.seat_columns[0]) or 24 | (self.seat_columns is None and other.seat_columns is None)) and 25 | self.price == other.price) 26 | 27 | def __hash__(self): 28 | return hash((self.section, 29 | self.row, 30 | self.seat_columns[0] if type(self.seat_columns) is list and len( 31 | self.seat_columns) > 0 else None, 32 | self.price)) 33 | -------------------------------------------------------------------------------- /apps/ticketscraping/prepare_reese84token.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | import requests 5 | import subprocess 6 | from . import constants 7 | 8 | 9 | def getReese84Token()->tuple[str, int]: 10 | def readFileContentToString(filename): 11 | f = open(filename, 'r') 12 | content = f.read() 13 | f.close() 14 | return content 15 | 16 | # fetch the javascript that generates the reese84 17 | antibot_js_code_full = requests.get(constants.ANTIBOT_JS_CODE_URL).text 18 | 19 | # trim the code to the function that is only used 20 | match_obj = re.search(constants.FN_MATCHING_REGEX, antibot_js_code_full) 21 | if not match_obj: 22 | raise Exception('reese84 manufacture fails') 23 | start, end = match_obj.span() 24 | antibot_js_code_trim = antibot_js_code_full[start:end] 25 | 26 | # inject the code to the javascript 27 | injector_js_code_loc = os.path.join( 28 | os.path.dirname(__file__), constants.INJECTOR_LOCATION) 29 | injector_header_js_code_loc = os.path.join(os.path.dirname( 30 | __file__), constants.INJECTOR_HEADER_LOCATION) 31 | injector_js_code, injector_header_js_code = readFileContentToString( 32 | injector_js_code_loc), readFileContentToString(injector_header_js_code_loc) 33 | runnable_js_code = injector_header_js_code + \ 34 | antibot_js_code_trim + injector_js_code 35 | 36 | # save the runnable js code 37 | runnable_file_loc = os.path.join(os.path.dirname( 38 | __file__), constants.RENNABLE_FILENAME) 39 | runnable_file = open(runnable_file_loc, "w") 40 | runnable_file.write(runnable_js_code) 41 | runnable_file.close() 42 | 43 | # run the js code using local node.js 44 | res = subprocess.run( 45 | ["node", runnable_file_loc], capture_output=True) 46 | token_str = res.stdout 47 | 48 | # produce the reese84 object 49 | token = json.loads(token_str) 50 | 51 | # invoke the get token api to get the reese84 token 52 | token_json_res = requests.post( 53 | constants.TOKEN_INTERROGATION_URL, headers=constants.BASIC_REQ_HEADER, json=token) 54 | json_obj = token_json_res.json() 55 | return json_obj['token'], json_obj['renewInSec'] 56 | -------------------------------------------------------------------------------- /apps/ticketscraping/schedulers/async_tasks_scheduler.py: -------------------------------------------------------------------------------- 1 | from ..connection.sender import Sender 2 | from ..constants import SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT 3 | 4 | async_tasks_scheduler = Sender(SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT) 5 | -------------------------------------------------------------------------------- /apps/ticketscraping/schedulers/mail_scheduler.py: -------------------------------------------------------------------------------- 1 | from ..connection.sender import Sender 2 | from ..constants import SERVICE_LOCALHOST, MAIL_RECEIVER_PORT 3 | 4 | 5 | mail_scheduler = Sender(SERVICE_LOCALHOST, MAIL_RECEIVER_PORT) 6 | -------------------------------------------------------------------------------- /apps/ticketscraping/scraping.py: -------------------------------------------------------------------------------- 1 | import sched 2 | import time 3 | import ctypes 4 | import random 5 | import requests 6 | import threading 7 | from . import constants 8 | from threading import Semaphore 9 | from .prepare_reese84token import getReese84Token 10 | from ..storage.storage import * 11 | from .seat_analysis import format_seats 12 | from .tasks.periodic import run_periodic_task 13 | import traceback 14 | 15 | class Reese84TokenUpdating(): 16 | def __init__(self): 17 | self.is_running = False 18 | self._reese84_token = '' 19 | self.reese84_renewInSec = 0 20 | self.token_access_semaphore = Semaphore(1) # one can access at a time 21 | self.token_semaphore = Semaphore(0) 22 | self.scheduler = sched.scheduler(time.time, time.sleep) 23 | 24 | @property 25 | def reese84_token(self): 26 | self.token_semaphore.acquire() 27 | self.token_access_semaphore.acquire() 28 | token = self._reese84_token 29 | self.token_semaphore.release() 30 | self.token_access_semaphore.release() 31 | 32 | return token 33 | 34 | def initialize_reese84_token(self): 35 | """ 36 | This method should not be called directly. 37 | """ 38 | self.token_access_semaphore.acquire() 39 | self._reese84_token, self.reese84_renewInSec = getReese84Token() 40 | self.token_semaphore.release() # produce a new token 41 | self.token_access_semaphore.release() 42 | self.scheduler.enter(self.reese84_renewInSec - 43 | constants.TOKEN_RENEW_SEC_OFFSET, constants.TOKEN_RENEW_PRIORITY, self.renew_reese84_token) 44 | 45 | def renew_reese84_token(self): 46 | """ 47 | This method should not be called directly. 48 | """ 49 | print("renewing token") 50 | self.token_semaphore.acquire() # invalidate a token 51 | self.token_access_semaphore.acquire() 52 | self._reese84_token, self.reese84_renewInSec = getReese84Token() 53 | self.token_semaphore.release() # produce a token 54 | self.token_access_semaphore.release() 55 | self.scheduler.enter(self.reese84_renewInSec - 56 | constants.TOKEN_RENEW_SEC_OFFSET, constants.TOKEN_RENEW_PRIORITY, self.renew_reese84_token) 57 | 58 | def start(self): 59 | # if the scheduler is already started - do nothing 60 | if self.is_running: 61 | return 62 | self.is_running = True 63 | self.initialize_reese84_token() 64 | self.scheduler.run() 65 | 66 | 67 | class TicketScraping(threading.Thread): 68 | def __init__(self, token_generator: Reese84TokenUpdating, event_id: str, subscribe_id: str, num_seats: int, target_price: int, tolerance: int, emails: list[str]): 69 | threading.Thread.__init__(self) 70 | self.is_running = False 71 | self.is_stopping = False 72 | self.event_id = event_id 73 | self.subscribe_id = subscribe_id 74 | self.num_seats = num_seats 75 | self.target_price = target_price 76 | self.tolerance = tolerance 77 | self.emails = emails 78 | self.token_gen = token_generator 79 | self.scheduler = sched.scheduler(time.time, time.sleep) 80 | self.initialDelay = random.randint( 81 | 1, constants.TICKET_SCRAPING_INTERVAL) 82 | 83 | def flag_for_termination(self): 84 | # cancel all scheduled jobs 85 | list(map(self.scheduler.cancel, self.scheduler.queue)) 86 | # raise exception to terminate the thread 87 | thread_id = self.get_id() 88 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 89 | ctypes.py_object(SystemExit)) 90 | print( 91 | f"Ticket scraping with subscription id={self.subscribe_id} marked for termination.") 92 | if res > 1: 93 | ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) 94 | print(f'Failed to terminate the thread with id={thread_id}') 95 | 96 | def ticket_scraping(self): 97 | # scrape the top-picks from ticketmaster 98 | top_picks_url = constants.get_top_picks_url(self.event_id) 99 | top_picks_q_params = constants.get_top_picks_query_params( 100 | self.num_seats, self.target_price, self.tolerance) 101 | top_picks_header = constants.get_top_picks_header() 102 | res = None 103 | try: 104 | res = requests.get(top_picks_url, headers=top_picks_header, params=top_picks_q_params, 105 | cookies=dict(reese84=self.token_gen.reese84_token)) # type: ignore 106 | except Exception: 107 | # retry after a delay 108 | self.scheduler.enter(constants.TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL, 109 | constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping) 110 | return 111 | # prune and format the received picks 112 | picks_obj = format_seats(res.json(), self.subscribe_id) 113 | 114 | # periodic task: update collections best_available_seats and best_history_seats 115 | # and automatically spawn async tasks 116 | run_periodic_task(picks_obj, self.subscribe_id, self.target_price, self.emails) 117 | 118 | print("Got the ticket info from TM. /", res.status_code) 119 | self.scheduler.enter(constants.TICKET_SCRAPING_INTERVAL, 120 | constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping) 121 | 122 | def get_id(self): 123 | # returns id of the respective thread 124 | if hasattr(self, '_thread_id'): 125 | return self._thread_id # type: ignore 126 | for id, thread in threading._active.items(): # type: ignore 127 | if thread is self: 128 | return id 129 | 130 | def run(self): 131 | try: 132 | # if the scheduler is already started - do nothing 133 | if self.is_running: 134 | return 135 | self.is_running = True 136 | self.is_stopping = False 137 | # randomize start time to scatter out event of API fetching 138 | time.sleep(self.initialDelay) 139 | self.ticket_scraping() 140 | self.scheduler.run() 141 | finally: 142 | print( 143 | f"Ticket scraping with subscription id={self.subscribe_id} has been terminated.") 144 | 145 | 146 | def start(): 147 | # reese84 token renewing thread 148 | reese_token_gen = Reese84TokenUpdating() 149 | serverThread_reese = threading.Thread(target=reese_token_gen.start) 150 | serverThread_reese.start() 151 | 152 | # ticket scraping threads 153 | scraping_list = dict() 154 | events = find_many(constants.DATABASE["EVENTS"], { 155 | '$or': [{'markPaused': {'$exists': False}}, {'markPaused': False}]}) 156 | for evt in events: 157 | ticket_scraping = TicketScraping( 158 | reese_token_gen, evt["tm_event_id"], evt["_id"], evt["ticket_num"], evt["target_price"], evt["tolerance"], evt["client_emails"]) 159 | print(ticket_scraping.initialDelay, "s") 160 | scraping_list[ticket_scraping.subscribe_id] = ticket_scraping 161 | for scraping_thread in scraping_list.values(): 162 | scraping_thread.start() 163 | 164 | # listen for changes in ticket scraping subscriptions 165 | while(True): 166 | with watch(constants.DATABASE["EVENTS"], pipeline=[{'$match': {'operationType': {'$in': ['delete', 'insert', 'replace', 'update']}}}], full_document='updateLookup') as stream: 167 | for change in stream: 168 | if change['operationType'] == "delete": 169 | # stop the thread 170 | doc_id = change['documentKey']['_id'] 171 | if doc_id in scraping_list: 172 | scraping_obj = scraping_list[doc_id] 173 | scraping_obj.flag_for_termination() 174 | del scraping_list[doc_id] 175 | elif change['operationType'] == "insert": 176 | # spawn a thread to do scraping operations 177 | full_doc = change['fullDocument'] 178 | ticket_scraping = TicketScraping( 179 | reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"], full_doc["client_emails"]) 180 | print(ticket_scraping.initialDelay, "s") 181 | scraping_list[ticket_scraping.subscribe_id] = ticket_scraping 182 | ticket_scraping.start() 183 | else: 184 | # replace or update - pause or resume ticket scraping 185 | full_doc = change['fullDocument'] 186 | doc_id = full_doc['_id'] 187 | if not 'markPaused' in full_doc: 188 | print("'markPaused' flag is unset, skip processing.") 189 | break 190 | if full_doc['markPaused'] == True: 191 | # pause scraping 192 | if doc_id in scraping_list: 193 | scraping_obj = scraping_list[doc_id] 194 | scraping_obj.flag_for_termination() 195 | del scraping_list[doc_id] 196 | else: 197 | # resume scraping if currently paused 198 | if doc_id not in scraping_list: 199 | ticket_scraping = TicketScraping( 200 | reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"], full_doc["client_emails"]) 201 | print(ticket_scraping.initialDelay, "s") 202 | scraping_list[ticket_scraping.subscribe_id] = ticket_scraping 203 | ticket_scraping.start() 204 | # display current number of ticket scraping 205 | print(f"{len(scraping_list)} ticket scraping threads now.") 206 | -------------------------------------------------------------------------------- /apps/ticketscraping/seat_analysis.py: -------------------------------------------------------------------------------- 1 | from dateutil import parser 2 | # from ..storage.storage import insert_one 3 | # from ..ticketscraping import constants 4 | 5 | 6 | def format_seats(data, subscriber_id): 7 | # prune top-picks data structure 8 | pruned_picks = prune_pick_attributes(data) 9 | 10 | # process seats data - use piping 11 | res = pipe([ 12 | append_scraping_config_ref, 13 | map_prices_to_seats, 14 | remove_embedded_field 15 | ], pruned_picks, subscriber_id) 16 | 17 | # # store in db 18 | # # print(res) 19 | # insert_one(constants.DATABASE['TOP_PICKS'], res) 20 | return res 21 | 22 | def pipe(fns: list, *args): 23 | out = args 24 | for fn in fns: 25 | if type(out) is tuple: 26 | out = fn(*out) 27 | else: 28 | out = fn(out) 29 | return out 30 | 31 | def get_value_from_map(map: dict, *args, **kwargs): 32 | # input validation 33 | if type(map) is not dict: 34 | return kwargs.get('default', None) 35 | res = kwargs.get('default', None) 36 | for attr in args: 37 | res = map.get(attr) 38 | if res is not None: 39 | break 40 | return res 41 | 42 | def get_value_from_nested_map(map: dict, *args, **kwargs): 43 | # input validation 44 | if type(map) is not dict: 45 | return kwargs.get('default', None) 46 | res = None 47 | m = map 48 | count = 0 49 | for attr in args: 50 | res = m.get(attr) 51 | count += 1 52 | if res is None: 53 | break 54 | elif type(res) is dict: 55 | m = res 56 | else: 57 | break 58 | return res if res is not None and count == len(args) else kwargs.get('default', None) 59 | 60 | def get_fn_return(fn, *args, **kwargs): 61 | res = kwargs.get('default', None) 62 | try: 63 | res = fn(*args) 64 | except: 65 | pass 66 | finally: 67 | return res 68 | 69 | def prune_pick_attributes(data): 70 | def prune_pick_offer_attributes(pick: dict): 71 | return { 72 | 'type': get_value_from_map(pick, 'type'), 73 | 'selection': get_value_from_map(pick, 'selection'), 74 | 'quality': get_value_from_map(pick, 'quality'), 75 | 'section': get_value_from_map(pick, 'section'), 76 | 'row': get_value_from_map(pick, 'row'), 77 | 'offerGroups': get_value_from_map(pick, 'offerGroups', 'offers'), 78 | 'area': get_value_from_map(pick, 'area'), 79 | 'maxQuantity': get_value_from_map(pick, 'maxQuantity'), 80 | } 81 | 82 | def prune_pick_embedded_attributes(embedded: dict): 83 | def prune_pick_embedded_offer_attributes(item): 84 | return { 85 | 'expired_date': get_fn_return(parser.parse, get_value_from_nested_map(item, 'meta', 'expires'), default=None), 86 | 'offerId': get_value_from_map(item, 'offerId'), 87 | 'rank': get_value_from_map(item, 'rank'), 88 | 'online': get_value_from_map(item, 'online'), 89 | 'protected': get_value_from_map(item, 'protected'), 90 | 'rollup': get_value_from_map(item, 'rollup'), 91 | 'inventoryType': get_value_from_map(item, 'inventoryType'), 92 | 'offerType': get_value_from_map(item, 'offerType'), 93 | 'currency': get_value_from_map(item, 'currency'), 94 | 'listPrice': get_value_from_map(item, 'listPrice'), 95 | 'faceValue': get_value_from_map(item, 'faceValue'), 96 | 'totalPrice': get_value_from_map(item, 'totalPrice'), 97 | 'noChargesPrice': get_value_from_map(item, 'noChargesPrice'), 98 | # 'listingId': get_value_from_map(item, 'listingId'), 99 | # 'listingVersionId': get_value_from_map(item, 'listingVersionId'), 100 | # 'charges': get_value_from_map(item, 'charges'), 101 | # 'sellableQuantities': get_value_from_map(item, 'sellableQuantities'), 102 | # 'section': get_value_from_map(item, 'section'), 103 | # 'row': get_value_from_map(item, 'row'), 104 | # 'seatFrom': get_value_from_map(item, 'seatFrom'), 105 | # 'seatTo': get_value_from_map(item, 'seatTo'), 106 | # 'ticketTypeId': get_value_from_map(item, 'ticketTypeId') 107 | } 108 | return { 109 | 'offer': list(map(prune_pick_embedded_offer_attributes, get_value_from_map(embedded, 'offer', default=dict()))) 110 | } 111 | return { 112 | 'expired_date': get_fn_return(parser.parse, get_value_from_nested_map(data, 'meta', 'expires'), default=None), 113 | 'eventId': get_value_from_map(data, 'eventId'), 114 | 'offset': get_value_from_map(data, 'offset'), 115 | 'total': get_value_from_map(data, 'total'), 116 | 'picks': list(map(prune_pick_offer_attributes, get_value_from_map(data, 'picks', default=dict()))), 117 | '_embedded': prune_pick_embedded_attributes(get_value_from_map(data, '_embedded', default=dict())) 118 | } 119 | 120 | 121 | def append_scraping_config_ref(data, config_id): 122 | data['scraping_config_ref'] = config_id 123 | return data 124 | 125 | 126 | def map_prices_to_seats(data): 127 | def map_prices_to_seat_helper(offer_table: dict): 128 | def __map_prices_to_seat_helper(pick): 129 | offerGroups = pick['offerGroups'] 130 | if offerGroups is None or len(offerGroups) == 0: 131 | return {'offer_available': False} 132 | offerGroup = offerGroups[0] 133 | offerIds = get_value_from_map(offerGroup, 'offers', default=[offerGroup]) 134 | offerSeatCols = get_value_from_map(offerGroup, 'seats') 135 | if len(offerIds) == 0: 136 | return {'offer_available': False} 137 | offerId = offerIds[0] 138 | offerObj = offer_table.get(offerId) 139 | res = {**pick, 'offer': offerObj, 'seat_columns': offerSeatCols} 140 | del res['offerGroups'] 141 | return res 142 | return __map_prices_to_seat_helper 143 | 144 | offer_dict = {offer['offerId']: offer for offer in data['_embedded']['offer']} 145 | picks_list = list( 146 | map(map_prices_to_seat_helper(offer_dict), data['picks'])) 147 | data['picks'] = picks_list 148 | return data 149 | 150 | def remove_embedded_field(data): 151 | del data['_embedded'] 152 | return data 153 | -------------------------------------------------------------------------------- /apps/ticketscraping/tasks/asynchronous.py: -------------------------------------------------------------------------------- 1 | from .. import constants 2 | from ..models.pick import Pick 3 | import typing, threading 4 | from .util.decorators import StorageInsertionOrder, wrap_fn_return 5 | from .strategies.quarters_seats import QuartersSeats 6 | from ..schedulers.mail_scheduler import mail_scheduler 7 | 8 | 9 | def run_async_tasks(picks: typing.Iterable[Pick], scraping_id: str, target_price: int, emails: list[str]): 10 | picks_size = len(list(picks)) 11 | if picks_size == 0: return 12 | 13 | store = StorageInsertionOrder(size=picks_size) 14 | def fn(arg: tuple[int, Pick]): 15 | idx, pick = arg 16 | thread = threading.Thread(target=wrap_fn_return(run_async_task, store.set, idx), args=(pick, scraping_id, target_price)) 17 | thread.start() 18 | return thread 19 | 20 | threads = list(map(fn, enumerate(picks))) 21 | 22 | for thread in threads: thread.join() 23 | 24 | # filter out new seats that do not need alerts 25 | store.filter() 26 | # better seats first 27 | store.sort() 28 | # get seats used in alerting 29 | alert_seats = store.sublist(0, min(store.size, constants.ALERT_SEATS_MAX_COUNT)) 30 | # get the alert information 31 | alert_contents = list(map(lambda qs: qs.get_alert_content(), alert_seats)) # type: ignore 32 | # send the alert to user 33 | mail_scheduler.send(emails, alert_contents) 34 | 35 | 36 | def run_async_task(pick: Pick, scraping_id: str, target_price: int): 37 | # Alert the user based on alert conditions 38 | qs = QuartersSeats(pick, scraping_id, target_price) 39 | if not qs.shouldAlert(): 40 | return None 41 | 42 | # success 43 | return qs 44 | -------------------------------------------------------------------------------- /apps/ticketscraping/tasks/periodic.py: -------------------------------------------------------------------------------- 1 | from ...storage.storage import find_many, insert_many, delete_many 2 | from ...ticketscraping import constants 3 | from ..models.pick import Pick 4 | from ..schedulers.async_tasks_scheduler import async_tasks_scheduler 5 | 6 | def generate_picks_set_from_picks(picks): 7 | def __helper(pick: dict): 8 | return Pick(_id=pick.get('_id'), 9 | scraping_id=pick.get('scraping_id'), 10 | type=pick['type'], 11 | selection=pick['selection'], 12 | quality=pick['quality'], 13 | section=pick['section'], 14 | row=pick['row'], 15 | area=pick['area'], 16 | maxQuantity=pick['maxQuantity'], 17 | offer=pick['offer'], 18 | seat_columns=pick['seat_columns']) 19 | 20 | if type(picks) is dict: 21 | return set(map(__helper, picks['picks'])) 22 | elif type(picks) is list: 23 | return set(map(__helper, picks)) 24 | else: 25 | raise Exception('argument type error') 26 | 27 | def get_current_best_available(scraping_id: str): 28 | return find_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], {"scraping_id": scraping_id}) 29 | def remove_best_seats(seats: set[Pick]): 30 | ids = [] 31 | for seat in seats: 32 | ids.append(seat._id) 33 | return delete_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], {"_id" : {"$in": ids}}) 34 | def insert_best_seats(seats: set[Pick], scraping_id: str): 35 | for seat in seats: 36 | seat.setScrapingId(scraping_id) 37 | return insert_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], list(map(lambda seat: vars(seat), seats))) 38 | def insert_history_seats(seats: set[Pick]): 39 | return insert_many(constants.DATABASE['BEST_HISTORY_SEATS'], list(map(lambda seat: vars(seat), seats))) 40 | 41 | 42 | def run_periodic_task(picks, scraping_id: str, target_price: int, emails: list[str]): 43 | # B the list of new best available seats 44 | new_best_avail = generate_picks_set_from_picks(picks) 45 | # A be the list of current best available seats 46 | cur_best_avail = generate_picks_set_from_picks(get_current_best_available(scraping_id)) 47 | 48 | # Compute C := A-B which is the seats 49 | overwritten_seats = cur_best_avail - new_best_avail 50 | 51 | # Compute D := B-A which is the new seats 52 | new_seats = new_best_avail - cur_best_avail 53 | 54 | print(f"size of B is {len(new_best_avail)}") 55 | print(f"size of A is {len(cur_best_avail)}") 56 | print(f"size of C is {len(overwritten_seats)}") 57 | print(f"size of D is {len(new_seats)}") 58 | 59 | # Remove C from best_available_seats 60 | remove_best_seats(overwritten_seats) 61 | 62 | # Insert D to best_available_seats 63 | insert_best_seats(new_seats, scraping_id) 64 | 65 | # Save C to best_history_seats. 66 | insert_history_seats(overwritten_seats) 67 | 68 | # Use D to invoke a handler to analyze them against the best_history_seats asynchronously. 69 | async_tasks_scheduler.send(new_seats, scraping_id, target_price, emails) 70 | -------------------------------------------------------------------------------- /apps/ticketscraping/tasks/strategies/quarters_seats.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | from ....storage.storage import count_docs, find_many 3 | from ....storage.query import find_max, find_min, find_many_ascending_order 4 | from ....ticketscraping import constants 5 | from ...models.pick import Pick 6 | from ..util.math import percentile 7 | from ....pushnotification.msg_formatter import format_entire_mail 8 | 9 | class QuartersSeats(): 10 | top_better_history_seats_sort = [ 11 | ('rank', pymongo.DESCENDING), ('price', pymongo.ASCENDING)] 12 | 13 | def __init__(self, pick: Pick, scraping_id: str, target_price: int): 14 | self._pick = pick 15 | self._scraping_id = scraping_id 16 | self.target_price = target_price 17 | self.percentile = 1.0 18 | self._num_before = 0 19 | self._num_total = 0 20 | 21 | def __lt__(self, other): 22 | # smaller the percentile, better the pick 23 | return self.percentile > other.percentile 24 | 25 | def __eq__(self, other): 26 | return self.percentile == other.percentile 27 | 28 | @property 29 | def pick(self): 30 | return self._pick 31 | 32 | @property 33 | def scraping_id(self): 34 | return self._scraping_id 35 | 36 | def get_alert_content(self): 37 | # alert user with content 38 | # new seat info (price, sec, row?, seats?) 39 | # new seat rank, total, and percentile 40 | cur_rank_new_seat = self._num_before + 1 41 | cur_total = self._num_total + 1 42 | percentile = self.percentile 43 | # top best history seats: used for comparison 44 | top_best_history_seats = self.get_top_better_history_seats() 45 | # all history seats: used for comparison 46 | 47 | # exact same seat in the history based on (sec, row?, seat?) 48 | same_seats = self.get_exact_same_seats() 49 | # notify user with info 50 | return format_entire_mail(pick=self.pick, target_price=self.target_price, percentile=percentile, rank=cur_rank_new_seat, num_total=cur_total, top_history_seats=top_best_history_seats, same_seats=same_seats) 51 | 52 | def get_top_better_history_seats(self, limit=constants.TOP_COMPARED_HISTORY_SEATS): 53 | return self.find_better_history_seats(sort=self.top_better_history_seats_sort, limit=limit) 54 | 55 | def shouldAlert(self): 56 | try: 57 | # Find price match 58 | self.target_price_metric() 59 | # Find enough history seats data 60 | percentile = self.percentile_metric() 61 | # Find the % of change 62 | percent_change = self.percent_of_change_metric() 63 | # Find percentile of seats in quarters 64 | self.quarter_percentile_metric() 65 | except Exception as ex: 66 | print(ex) 67 | return False 68 | 69 | # success 70 | print(f"percent change out of max-min: {percent_change*100}") 71 | print(f"all history seats percentile: {percentile*100}") 72 | print( 73 | f"new seat - price: {self.pick.price} rank: {self.pick.quality} section: {self.pick.section} row: {self.pick.row}") 74 | print(f"quarters history seats percentile: {self.percentile*100}") 75 | return True 76 | 77 | 78 | def quarter_percentile_metric(self): 79 | if self.get_percentile() > constants.PERCENTILE_HISTORY_PRICES: 80 | raise Exception('the seat is not recommended') 81 | 82 | def get_percentile(self): 83 | self._num_before = self.count_better_history_seats() 84 | self._num_total = self.count_quarters_history_seats() 85 | if self._num_total < constants.MINIMUM_HISTORY_DATA: 86 | raise Exception('no enough history seats data(count < 3)') 87 | self.percentile = percentile(self._num_before, self._num_total) 88 | return self.percentile 89 | 90 | def count_quarters_history_seats(self): 91 | filter_obj = self.__get_quarters_history_seats_filter__() 92 | return count_docs(constants.DATABASE['BEST_HISTORY_SEATS'], filter_obj) 93 | 94 | def __get_quarters_history_seats_filter__(self): 95 | return { 96 | "scraping_id": self._scraping_id, 97 | "$or": [ 98 | { 99 | "price": {"$lte": self._pick.price}, 100 | "quality": {"$gte": self._pick.quality}, 101 | }, 102 | { 103 | "price": {"$gt": self._pick.price}, 104 | "quality": {"$lt": self._pick.quality}, 105 | } 106 | ] 107 | } 108 | 109 | def __get_better_history_seats_filter__(self): 110 | return { 111 | "scraping_id": self._scraping_id, 112 | "price": {"$lte": self._pick.price}, 113 | "quality": {"$gte": self._pick.quality}, 114 | } 115 | 116 | def find_better_history_seats(self, **kwargs): 117 | limit = kwargs.get('limit') 118 | sort_seq = kwargs.get('sort') 119 | filter_obj = self.__get_better_history_seats_filter__() 120 | return find_many(constants.DATABASE['BEST_HISTORY_SEATS'], filter_obj, sort=sort_seq, limit=limit) 121 | 122 | def count_better_history_seats(self): 123 | filter_obj = self.__get_better_history_seats_filter__() 124 | return count_docs(constants.DATABASE['BEST_HISTORY_SEATS'], filter_obj) 125 | 126 | def target_price_metric(self): 127 | # exceed target price - abort 128 | if self.pick.price > self.target_price: 129 | raise Exception('price of the seat is not low enough') 130 | 131 | 132 | def percent_of_change_metric(self) -> float: 133 | # Find the % of change 134 | max_seat = find_max(constants.DATABASE['BEST_HISTORY_SEATS'], { 135 | "scraping_id": self.scraping_id}, 'price') 136 | min_seat = find_min(constants.DATABASE['BEST_HISTORY_SEATS'], { 137 | "scraping_id": self.scraping_id}, 'price') 138 | max_price = 0 if type(max_seat) is not dict else max_seat.get('price', 0) 139 | min_price = 0 if type(min_seat) is not dict else min_seat.get('price', 0) 140 | 141 | # min and max are identical - abort 142 | if max_price == min_price: 143 | raise Exception('min and max prices are identical') 144 | 145 | percent_change = (self.pick.price - min_price) / (max_price - min_price) 146 | 147 | # # price of change exceeds the metric value - abort 148 | # if percent_change > constants.PERCENT_OF_CHANGE: 149 | # raise Exception( 150 | # f'price of change ({percent_change}) exceeds the metric value') 151 | 152 | return percent_change 153 | 154 | 155 | def percentile_metric(self) -> float: 156 | rank = count_docs(constants.DATABASE['BEST_HISTORY_SEATS'], 157 | {"scraping_id": self.scraping_id, "price": {"$lte": self.pick.price}}) 158 | total_count = count_docs(constants.DATABASE['BEST_HISTORY_SEATS'], 159 | { 160 | "scraping_id": self.scraping_id}) 161 | 162 | # no history seats data - abort 163 | if total_count < constants.MINIMUM_HISTORY_DATA: 164 | raise Exception('no enough history seats data (count < 3)') 165 | 166 | percentile = rank / total_count 167 | 168 | # # percentile of history prices exceeds the metric value - abort 169 | # if percentile > constants.PERCENTILE_HISTORY_PRICES: 170 | # raise Exception( 171 | # 'percentile of history prices ({percentile}) exceeds the metric value') 172 | 173 | return percentile 174 | 175 | 176 | def get_exact_same_seats(self): 177 | return find_many_ascending_order(constants.DATABASE['BEST_HISTORY_SEATS'], 178 | {"scraping_id": self.scraping_id, "section": self.pick.section, 179 | "row": self.pick.row, "seat_columns": self.pick.seat_columns}, 180 | 'last_modified') 181 | -------------------------------------------------------------------------------- /apps/ticketscraping/tasks/util/decorators.py: -------------------------------------------------------------------------------- 1 | class StorageInsertionOrder: 2 | def __init__(self, size=0): 3 | self.store = [0] * size 4 | 5 | def __iter__(self): 6 | return iter(self.store) 7 | 8 | @property 9 | def size(self): 10 | return len(self.store) 11 | 12 | def add(self, item): 13 | self.store.append(item) 14 | 15 | def get(self, index: int): 16 | return self.store[index] 17 | 18 | def set(self, index: int, item): 19 | self.store[index] = item 20 | 21 | def sort(self): 22 | sorted(self.store) 23 | 24 | def filter(self): 25 | self.store = list(filter(lambda item: item is not None, self.store)) 26 | 27 | def sublist(self, from_idx: int, to_idx: int): 28 | return self.store[from_idx:to_idx] 29 | 30 | 31 | def wrap_fn_return(fn, storing_fn, index): 32 | def inner_fn(*args, **kwargs): 33 | res = fn(*args, **kwargs) 34 | storing_fn(index, res) 35 | return res 36 | return inner_fn -------------------------------------------------------------------------------- /apps/ticketscraping/tasks/util/math.py: -------------------------------------------------------------------------------- 1 | def percentile(num_before: int, total: int) -> float: 2 | denominator = total 3 | if denominator == 0: 4 | denominator = 1 5 | return num_before / denominator 6 | 7 | def percentileInBetween(num_before: int, num_after: int)->float: 8 | denominator = num_before + num_after + 1 9 | return percentile(num_before, denominator) 10 | -------------------------------------------------------------------------------- /apps/trackerapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jackiebibili/ticket_tracker_api/d4f8451d49d6de4d9d7e1532b2496961e19c08db/apps/trackerapi/__init__.py -------------------------------------------------------------------------------- /apps/trackerapi/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/trackerapi/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TrackerapiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.trackerapi' 7 | -------------------------------------------------------------------------------- /apps/trackerapi/error_handler.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | 3 | # custom exceptions 4 | class BadRequestException(Exception): 5 | pass 6 | 7 | # error handler decorator 8 | def wrap_error_handler(fn): 9 | def inner_fn(*args, **kwargs): 10 | try: 11 | return fn(*args, **kwargs) 12 | except BadRequestException as ex: 13 | # bad request 14 | return JsonResponse({"error": str(ex)}, status=400) 15 | except Exception as ex: 16 | # internal server error 17 | return JsonResponse({"error": "something went wrong."}, status=500) 18 | return inner_fn -------------------------------------------------------------------------------- /apps/trackerapi/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jackiebibili/ticket_tracker_api/d4f8451d49d6de4d9d7e1532b2496961e19c08db/apps/trackerapi/migrations/__init__.py -------------------------------------------------------------------------------- /apps/trackerapi/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /apps/trackerapi/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/trackerapi/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('subscribe/', views.subscribe_tm_event_price_tracking), 6 | path('unsubscribe/', views.unsubscribe_tm_event_price_tracking) 7 | ] -------------------------------------------------------------------------------- /apps/trackerapi/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse, HttpRequest 3 | from ..storage.storage import insert_one, find_one_and_update 4 | from .error_handler import BadRequestException, wrap_error_handler 5 | from ..ticketscraping import constants 6 | import json 7 | 8 | 9 | # Create your views here. 10 | @wrap_error_handler 11 | def subscribe_tm_event_price_tracking(req: HttpRequest): 12 | if req.method == 'POST': 13 | body = json.loads(req.body) 14 | # validation 15 | for key in constants.SUBSCRIBE_REQUEST_PROPS.values(): 16 | if key not in body: 17 | raise BadRequestException('Request is invalid.') 18 | # validation 19 | target_price = body["target_price"] 20 | tolerance = body["tolerance"] 21 | if target_price - tolerance < 0: 22 | raise BadRequestException('Lowest price cannot be negative.') 23 | 24 | doc = constants.filter_obj_from_attrs(body, constants.SUBSCRIBE_REQUEST_PROPS) 25 | 26 | insert_one(constants.DATABASE['EVENTS'], doc) 27 | return HttpResponse('OK', status=200) 28 | 29 | 30 | @wrap_error_handler 31 | def unsubscribe_tm_event_price_tracking(req: HttpRequest): 32 | if req.method == 'POST': 33 | body = json.loads(req.body) 34 | id = body['subscription_id'] 35 | 36 | res = find_one_and_update(constants.DATABASE['EVENTS'], {"_id": id}, {'$set': {'markPaused': True}}) 37 | if res: 38 | return HttpResponse('OK', status=200) 39 | else: 40 | raise BadRequestException('Subscription id not found.') 41 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | parameter-store: 5 | build_ssh_key: "/Dev/Production/tm-tracker-api/codebuild_ssh_key" 6 | phases: 7 | install: 8 | commands: 9 | - export DEPLOY_SCRIPT=/home/ec2-user/tm_tracker_api/prod.sh 10 | - mkdir -p ~/.ssh 11 | - echo "$build_ssh_key" > ~/.ssh/id_rsa 12 | - chmod 600 ~/.ssh/id_rsa 13 | pre_build: 14 | commands: 15 | - echo Logging into aws ECR... 16 | - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com 17 | build: 18 | commands: 19 | - echo Build started on `date` 20 | - echo Building the Docker image... 21 | - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . 22 | - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 23 | post_build: 24 | commands: 25 | - echo Build completed on `date` 26 | - echo Pushing the Docker image... 27 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 28 | - echo Deploying the Docker image... 29 | - ssh -o "StrictHostKeyChecking no" $SERVER_URL $DEPLOY_SCRIPT 30 | 31 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tmtracker.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /secret.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CONN_SRV = os.environ.get('CONN_SRV') 4 | -------------------------------------------------------------------------------- /tmtracker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jackiebibili/ticket_tracker_api/d4f8451d49d6de4d9d7e1532b2496961e19c08db/tmtracker/__init__.py -------------------------------------------------------------------------------- /tmtracker/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for tmtracker project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tmtracker.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /tmtracker/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tmtracker project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = False 24 | 25 | ALLOWED_HOSTS = ['*'] 26 | 27 | # Application definition 28 | 29 | INSTALLED_APPS = [ 30 | # 'django.contrib.admin', 31 | # 'django.contrib.auth', 32 | 'django.contrib.contenttypes', 33 | # 'django.contrib.messages', 34 | 'django.contrib.staticfiles', 35 | 'apps.startup', 36 | 'apps.trackerapi', 37 | ] 38 | 39 | MIDDLEWARE = [ 40 | 'django.middleware.security.SecurityMiddleware', 41 | # 'django.contrib.sessions.middleware.SessionMiddleware', 42 | 'django.middleware.common.CommonMiddleware', 43 | # 'django.middleware.csrf.CsrfViewMiddleware', 44 | # 'django.contrib.auth.middleware.AuthenticationMiddleware', 45 | # 'django.contrib.messages.middleware.MessageMiddleware', 46 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 47 | ] 48 | 49 | REST_FRAMEWORK = { 50 | 'DEFAULT_AUTHENTICATION_CLASSES': [], 51 | 'DEFAULT_PERMISSION_CLASSES': [], 52 | 'UNAUTHENTICATED_USER': None 53 | } 54 | 55 | ROOT_URLCONF = 'tmtracker.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'tmtracker.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 78 | 79 | # DATABASES = { 80 | # 'default': { 81 | # 'ENGINE': 'django.db.backends.sqlite3', 82 | # 'NAME': BASE_DIR / 'db.sqlite3', 83 | # } 84 | # } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 120 | 121 | STATIC_URL = 'static/' 122 | STATIC_ROOT = 'static/' 123 | 124 | # Default primary key field type 125 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 126 | 127 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 128 | -------------------------------------------------------------------------------- /tmtracker/urls.py: -------------------------------------------------------------------------------- 1 | """tmtracker URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.urls import path, include 17 | 18 | urlpatterns = [ 19 | path('tracker-api/', include('apps.trackerapi.urls')) 20 | ] 21 | -------------------------------------------------------------------------------- /tmtracker/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tmtracker project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tmtracker.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from pymongo.mongo_client import MongoClient 2 | from secret import CONN_SRV 3 | 4 | client = MongoClient(CONN_SRV) 5 | print("=== database connection is established ===") 6 | 7 | def get_db_handle(db_name): 8 | global client 9 | db_handle = client[db_name] 10 | return db_handle 11 | --------------------------------------------------------------------------------