Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0d045845 | ||
|
|
65fbb6fb5e | ||
|
|
1dee214ec1 | ||
|
|
64c485e1d8 | ||
|
|
daf0b7466c | ||
|
|
8bfe1e75d2 | ||
|
|
4ff5bacd13 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
venv
|
||||
__pycache__
|
||||
env.py
|
||||
env.slave.py
|
||||
env.docker.py
|
||||
docker-compose.yml
|
||||
docker-compose.yml
|
||||
config.toml
|
||||
config_test.toml
|
||||
data/db*
|
||||
@@ -1,6 +0,0 @@
|
||||
ports:
|
||||
- port: 8008
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: pip install -r requirements.txt && cp env.py.example env.py
|
||||
command: python main.py -t
|
||||
16
CHANGELOG.md
Normal file
16
CHANGELOG.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 更新日志
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [V2.0.0] - 2019-12-31
|
||||
### Added
|
||||
- 重构代码结构
|
||||
- 支持使用 sqlite, mysql 数据库
|
||||
- 支持使用代理进行余票查询
|
||||
- 添加小黑屋支持,避免重复下单
|
||||
- 添加 Pipenv 支持
|
||||
- 添加单元测试用例
|
||||
|
||||
### Removed
|
||||
- 移除 CDN 支持
|
||||
- 移除 Web 界面(待重写)
|
||||
@@ -14,6 +14,6 @@ VOLUME /data
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY env.docker.py.example /config/env.py
|
||||
COPY config.toml.example config.toml
|
||||
|
||||
CMD [ "python", "main.py" , "-c", "/config/env.py"]
|
||||
CMD [ "python", "main.py"]
|
||||
|
||||
18
Pipfile
Normal file
18
Pipfile
Normal file
@@ -0,0 +1,18 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
ipython = "*"
|
||||
|
||||
[packages]
|
||||
toml = "*"
|
||||
redis = "*"
|
||||
tortoise-orm = "==0.15.4"
|
||||
aiohttp = "*"
|
||||
aioredis = "*"
|
||||
aiomysql = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
415
Pipfile.lock
generated
Normal file
415
Pipfile.lock
generated
Normal file
@@ -0,0 +1,415 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "66b4286bb6eb49e7de326a6a02aa8bec298691068e41d9b73dd28ab3e6c044df"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
|
||||
"sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
|
||||
"sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
|
||||
"sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
|
||||
"sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
|
||||
"sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
|
||||
"sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
|
||||
"sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
|
||||
"sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
|
||||
"sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
|
||||
"sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
|
||||
"sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.6.2"
|
||||
},
|
||||
"aiomysql": {
|
||||
"hashes": [
|
||||
"sha256:5fd798481f16625b424eec765c56d712ac78a51f3bd0175a3de94107aae43307",
|
||||
"sha256:d89ce25d44dadb43cf2d9e4603bd67b7a0ad12d5e67208de013629ba648df2ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.20"
|
||||
},
|
||||
"aioredis": {
|
||||
"hashes": [
|
||||
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
|
||||
"sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"aiosqlite": {
|
||||
"hashes": [
|
||||
"sha256:4f02314a42db6722dc26f2a6119c64e3f05f141f57bbf2b1e1f9fd741b6d7fb8"
|
||||
],
|
||||
"version": "==0.11.0"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
|
||||
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
|
||||
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
|
||||
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
|
||||
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
|
||||
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
|
||||
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
|
||||
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
|
||||
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
|
||||
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
|
||||
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
|
||||
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
|
||||
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
|
||||
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
|
||||
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
|
||||
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
|
||||
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
|
||||
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
|
||||
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
|
||||
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
|
||||
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
|
||||
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
|
||||
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
|
||||
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
|
||||
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
|
||||
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
|
||||
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
|
||||
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
|
||||
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
|
||||
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
|
||||
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
|
||||
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
|
||||
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
|
||||
],
|
||||
"version": "==1.13.2"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"ciso8601": {
|
||||
"hashes": [
|
||||
"sha256:307342e8bb362ae41a3f3a089c11b374116823bce6fbe5d784e2a2dc37f2c753"
|
||||
],
|
||||
"version": "==2.1.2"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
|
||||
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
|
||||
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
|
||||
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
|
||||
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
|
||||
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
|
||||
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
|
||||
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
|
||||
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
|
||||
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
|
||||
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
|
||||
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
|
||||
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
|
||||
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
|
||||
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
|
||||
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
|
||||
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
|
||||
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
|
||||
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
|
||||
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
|
||||
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"hiredis": {
|
||||
"hashes": [
|
||||
"sha256:01b577f84c20ecc9c07fc4c184231b08e3c3942de096fa99978e053de231c423",
|
||||
"sha256:01ff0900134166961c9e339df77c33b72f7edc5cb41739f0babcd9faa345926e",
|
||||
"sha256:03ed34a13316d0c34213c4fd46e0fa3a5299073f4d4f08e93fed8c2108b399b3",
|
||||
"sha256:040436e91df5143aff9e0debb49530d0b17a6bd52200ce568621c31ef581b10d",
|
||||
"sha256:091eb38fbf968d1c5b703e412bbbd25f43a7967d8400842cee33a5a07b33c27b",
|
||||
"sha256:102f9b9dc6ed57feb3a7c9bdf7e71cb7c278fe8df1edfcfe896bc3e0c2be9447",
|
||||
"sha256:2b4b392c7e3082860c8371fab3ae762139090f9115819e12d9f56060f9ede05d",
|
||||
"sha256:2c9cc0b986397b833073f466e6b9e9c70d1d4dc2c2c1b3e9cae3a23102ff296c",
|
||||
"sha256:2fa65a9df683bca72073cd77709ddeb289ea2b114d3775d225fbbcc5faf808c5",
|
||||
"sha256:38437a681f17c975fd22349e72c29bc643f8e7eb2d6dc5df419eac59afa4d7ce",
|
||||
"sha256:3b3428fa3cf1ee178807b52c9bee8950ab94cd4eaa9bfae8c1bbae3c49501d34",
|
||||
"sha256:3dd8c2fae7f5494978facb0e93297dd627b1a3f536f3b070cf0a7d9157a07dcb",
|
||||
"sha256:4414a96c212e732723b5c3d7c04d386ebbb2ec359e1de646322cbc3f875cbd0d",
|
||||
"sha256:48c627581ad4ef60adbac980981407939acf13a0e18f093502c7b542223c4f19",
|
||||
"sha256:4a60e71625a2d78d8ab84dfb2fa2cfd9458c964b6e6c04fea76d9ade153fb371",
|
||||
"sha256:585ace09f434e43d8a8dbeb366865b1a044d7c06319b3c7372a0a00e63b860f4",
|
||||
"sha256:74b364b3f06c9cf0a53f7df611045bc9437ed972a283fa1f0b12537236d23ddc",
|
||||
"sha256:75c65c3850e89e9daa68d1b9bedd5806f177d60aa5a7b0953b4829481cfc1f72",
|
||||
"sha256:7f052de8bf744730a9120dbdc67bfeb7605a01f69fb8e7ba5c475af33c24e145",
|
||||
"sha256:8113a7d5e87ecf57cd4ae263cc9e429adb9a3e59f5a7768da5d3312a8d0a051a",
|
||||
"sha256:84857ce239eb8ed191ac78e77ff65d52902f00f30f4ee83bf80eb71da73b70e6",
|
||||
"sha256:8644a48ddc4a40b3e3a6b9443f396c2ee353afb2d45656c4fc68d04a82e8e3f7",
|
||||
"sha256:936aa565e673536e8a211e43ec43197406f24cd1f290138bd143765079c8ba00",
|
||||
"sha256:9afeb88c67bbc663b9f27385c496da056d06ad87f55df6e393e1516cfecb0461",
|
||||
"sha256:9d62cc7880110e4f83b0a51d218f465d3095e2751fbddd34e553dbd106a929ff",
|
||||
"sha256:a1fadd062fc8d647ff39220c57ea2b48c99bb73f18223828ec97f88fc27e7898",
|
||||
"sha256:a7754a783b1e5d6f627c19d099b178059c62f782ab62b4d8ba165b9fbc2ee34c",
|
||||
"sha256:aa59dd63bb3f736de4fc2d080114429d5d369dfb3265f771778e8349d67a97a4",
|
||||
"sha256:ae2ee0992f8de249715435942137843a93db204dd7db1e7cc9bdc5a8436443e8",
|
||||
"sha256:b36842d7cf32929d568f37ec5b3173b72b2ec6572dec4d6be6ce774762215aee",
|
||||
"sha256:bcbf9379c553b5facc6c04c1e5569b44b38ff16bcbf354676287698d61ee0c92",
|
||||
"sha256:cbccbda6f1c62ab460449d9c85fdf24d0d32a6bf45176581151e53cc26a5d910",
|
||||
"sha256:d0caf98dfb8af395d6732bd16561c0a2458851bea522e39f12f04802dbf6f502",
|
||||
"sha256:d6456afeddba036def1a36d8a2758eca53202308d83db20ab5d0b66590919627",
|
||||
"sha256:dbaef9a21a4f10bc281684ee4124f169e62bb533c2a92b55f8c06f64f9af7b8f",
|
||||
"sha256:dce84916c09aaece006272b37234ae84a8ed13abb3a4d341a23933b8701abfb5",
|
||||
"sha256:eb8c9c8b9869539d58d60ff4a28373a22514d40495911451343971cb4835b7a9",
|
||||
"sha256:efc98b14ee3a8595e40b1425e8d42f5fd26f11a7b215a81ef9259068931754f4",
|
||||
"sha256:fa2dc05b87d97acc1c6ae63f3e0f39eae5246565232484b08db6bf2dc1580678",
|
||||
"sha256:fe7d6ce9f6a5fbe24f09d95ea93e9c7271abc4e1565da511e1449b107b4d7848"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:0f04bf4c15d8417401a10a650c349ccc0285943681bfd87d3690587d7714a9b4",
|
||||
"sha256:15a61c0df2d32487e06f6084eabb48fd9e8b848315e397781a70caf9670c9d78",
|
||||
"sha256:3c5e2dcbe6b04cbb4303e47a896757a77b676c5e5db5528be7ff92f97ba7ab95",
|
||||
"sha256:5d2b32b890d9e933d3ced417924261802a857abdee9507b68c75014482145c03",
|
||||
"sha256:5e5fb8bfebf87f2e210306bf9dd8de2f1af6782b8b78e814060ae9254ab1f297",
|
||||
"sha256:63ba2be08d82ea2aa8b0f7942a74af4908664d26cb4ff60c58eadb1e33e7da00",
|
||||
"sha256:73740fcdb38f0adcec85e97db7557615b50ec4e5a3e73e35878720bcee963382",
|
||||
"sha256:78bed18e7f1eb21f3d10ff3acde900b4d630098648fe1d65bb4abfb3e22c4900",
|
||||
"sha256:a02fade7b5476c4f88efe9593ff2f3286698d8c6d715ba4f426954f73f382026",
|
||||
"sha256:aacbde3a8875352a640efa2d1b96e5244a29b0f8df79cbf1ec6470e86fd84697",
|
||||
"sha256:be813fb9e5ce41a5a99a29cdb857144a1bd6670883586f995b940a4878dc5238",
|
||||
"sha256:bfcad6da0b8839f01a819602aaa5c5a5b4c85ecbfae9b261a31df3d9262fb31e",
|
||||
"sha256:c2bfc0db3166e68515bc4a2b9164f4f75ae9c793e9635f8651f2c9ffc65c8dad",
|
||||
"sha256:c66d11870ae066499a3541963e6ce18512ca827c2aaeaa2f4e37501cee39ac5d",
|
||||
"sha256:cc7f2202b753f880c2e4123f9aacfdb94560ba893e692d24af271dac41f8b8d9",
|
||||
"sha256:d1f45e5bb126662ba66ee579831ce8837b1fd978115c9657e32eb3c75b92973d",
|
||||
"sha256:ed5f3378c102257df9e2dc9ce6468dabf68bee9ec34969cfdc472631aba00316"
|
||||
],
|
||||
"version": "==4.7.3"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3",
|
||||
"sha256:fdbae4e6f505ece28ab3b96f5c103a1d87e42845d249c33a097f14abb2bdad3e"
|
||||
],
|
||||
"version": "==2.19"
|
||||
},
|
||||
"pymysql": {
|
||||
"hashes": [
|
||||
"sha256:95f057328357e0e13a30e67857a8c694878b0175797a9a203ee7adbfb9b1ec5f",
|
||||
"sha256:9ec760cbb251c158c19d6c88c17ca00a8632bac713890e465b2be01fdc30713f"
|
||||
],
|
||||
"version": "==0.9.2"
|
||||
},
|
||||
"pypika": {
|
||||
"hashes": [
|
||||
"sha256:a4d80829a065047c39b7838aafb440761bd9a306977c0e6f78caf5eaa056c9e4"
|
||||
],
|
||||
"version": "==0.35.18"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:3613daad9ce5951e426f460deddd5caf469e08a3af633e9578fc77d362becf62",
|
||||
"sha256:8d0fc278d3f5e1249967cba2eb4a5632d19e45ce5c09442b8422d15ee2c22cc2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.11"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"tortoise-orm": {
|
||||
"hashes": [
|
||||
"sha256:90d1c4cc2038bcfd8c436106a2a5e667354208b2f4db8a63b96a05e6125e73f5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.15.4"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2",
|
||||
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d",
|
||||
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"
|
||||
],
|
||||
"version": "==3.7.4.1"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce",
|
||||
"sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6",
|
||||
"sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce",
|
||||
"sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae",
|
||||
"sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d",
|
||||
"sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f",
|
||||
"sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b",
|
||||
"sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b",
|
||||
"sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb",
|
||||
"sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462",
|
||||
"sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea",
|
||||
"sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70",
|
||||
"sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1",
|
||||
"sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a",
|
||||
"sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b",
|
||||
"sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080",
|
||||
"sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appnope": {
|
||||
"hashes": [
|
||||
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
||||
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
||||
],
|
||||
"markers": "sys_platform == 'darwin'",
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"backcall": {
|
||||
"hashes": [
|
||||
"sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
|
||||
"sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
|
||||
],
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
"sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
|
||||
"sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
|
||||
],
|
||||
"version": "==4.4.1"
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:0b67031a659fd7046cbf8c4efc2a0d514ee050434dfede76f540cfa6f806f65e",
|
||||
"sha256:cc70e2111d1629e31ef9ff445529f8bb2b4fc4f93bfe140c33fe18cf36fde072"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.11.0"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
|
||||
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"jedi": {
|
||||
"hashes": [
|
||||
"sha256:1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064",
|
||||
"sha256:e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1",
|
||||
"sha256:5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3"
|
||||
],
|
||||
"version": "==0.5.2"
|
||||
},
|
||||
"pexpect": {
|
||||
"hashes": [
|
||||
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
|
||||
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==4.7.0"
|
||||
},
|
||||
"pickleshare": {
|
||||
"hashes": [
|
||||
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
|
||||
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
|
||||
],
|
||||
"version": "==0.7.5"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7",
|
||||
"sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990"
|
||||
],
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"ptyprocess": {
|
||||
"hashes": [
|
||||
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
|
||||
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
||||
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
||||
],
|
||||
"version": "==2.5.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
|
||||
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
|
||||
],
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"traitlets": {
|
||||
"hashes": [
|
||||
"sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
|
||||
"sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
|
||||
],
|
||||
"version": "==4.3.3"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
config.toml.example
Normal file
90
config.toml.example
Normal file
@@ -0,0 +1,90 @@
|
||||
[app]
|
||||
env = 'dev'
|
||||
debug = true
|
||||
query_interval = 1.5 # 查询间隔
|
||||
|
||||
[web]
|
||||
# Comming soon.
|
||||
ip = '0.0.0.0'
|
||||
port = 8081
|
||||
|
||||
[db]
|
||||
engine = 'sqlite' # 默认使用 sqlite,支持 mysql
|
||||
# host = '127.0.0.1'
|
||||
# port = 3306
|
||||
# user = 'user'
|
||||
# password = 'password'
|
||||
# database = 'py12306'
|
||||
|
||||
[[user]]
|
||||
enable = true
|
||||
id = 1 # 用户唯一 Id,数字,不可重复
|
||||
name= 'your 12306 account name'
|
||||
password = 'your 12306 password'
|
||||
|
||||
# 简单版本
|
||||
[[query]]
|
||||
id = 1
|
||||
user_id = 1
|
||||
left_dates = ['2020-01-25', '2019-01-26']
|
||||
members = ['宝玉', '宝钗']
|
||||
seats = ['硬卧', '硬座']
|
||||
train_numbers = ['K356', 'K1172', 'K4184']
|
||||
stations = ['北京', '深圳']
|
||||
|
||||
[[query]]
|
||||
enable = true # 是否启用,默认启用
|
||||
id = 2
|
||||
user_id = 0 # 将会使用指定的用户下单
|
||||
left_dates= [ '2020-01-25', '2020-01-26' ] # 出发日期
|
||||
stations = ['北京', '深圳', '北京', '广州'] # 车站 支持多个车站同时查询
|
||||
members = [ '宝玉', '黛玉' ] # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['贾琏', '贾琏']
|
||||
allow_less_member = false # 是否允许余票不足时提交部分乘客
|
||||
seats = [ '硬卧', '硬座' ] # 筛选座位 有先后顺序 可用值: 一等座, 二等座, 商务座, 特等座, 硬座, 软座, 硬卧, 二等卧, 软卧, 一等卧, 高级软卧, 动卧, 高级动卧, 无座
|
||||
train_numbers = [ 'K356', 'K1172', 'K4184' ] # 筛选车次 可以为空,为空则所有车次都可以提交 如 []
|
||||
except_train_numbers = [] # 筛选车次,排除车次 train_numbers 和 except_train_numbers 不可同时存在
|
||||
periods = ['00:00','24:00'] # 筛选时间
|
||||
|
||||
[redis]
|
||||
enable = false
|
||||
host = '127.0.0.1'
|
||||
port = 6379
|
||||
db = 1
|
||||
password = ''
|
||||
|
||||
[proxy]
|
||||
enable = false
|
||||
rate = 90 # 使用代理的比例
|
||||
concurrent_num = 5
|
||||
url = 'http://127.0.0.1:8081/get_ip?&rule=12306'
|
||||
|
||||
[notifaction.ding_talk]
|
||||
# 钉钉通知 使用说明 https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq
|
||||
enable = true
|
||||
access_token = ''
|
||||
|
||||
[notifaction.bark]
|
||||
# Bark 推送到ios设备 参考 https://www.v2ex.com/t/467407
|
||||
enable = false
|
||||
push_url = ''
|
||||
|
||||
[notifaction.email]
|
||||
# 邮箱配置
|
||||
enable = false
|
||||
sender = 'sender@example.com' # 邮件发送至
|
||||
to = 'receiver@example.com' # 邮件接受者 # 可以多个 [email1@gmail.com, email2@gmail.com]
|
||||
host = 'localhost' # 邮件服务 host
|
||||
user = ''
|
||||
password = ''
|
||||
|
||||
[notifaction.server_chan]
|
||||
# ServerChan 使用说明 http://sc.ftqq.com
|
||||
enable = false
|
||||
sckey = ''
|
||||
|
||||
[notifaction.ding_xing_voice]
|
||||
# 语音验证码 # 购买成功后到控制台找到 APPCODE 放在下面就可以了
|
||||
# 鼎信 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.e27e7218KQttQS
|
||||
enable = false
|
||||
appcode = ''
|
||||
phone = '13800138000'
|
||||
2268
data/cdn.txt
2268
data/cdn.txt
File diff suppressed because it is too large
Load Diff
1
data/stationList.json
Executable file
1
data/stationList.json
Executable file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,13 +1,12 @@
|
||||
version: "2"
|
||||
version: "3.4"
|
||||
services:
|
||||
py12306:
|
||||
build: .
|
||||
volumes:
|
||||
# - ./runtime:/code/runtime # 未使用 env.docker.py.example 可以打开此项
|
||||
- ./env.py:/config/env.py
|
||||
- ./config.toml:/code/config.toml
|
||||
- py12306:/data
|
||||
ports:
|
||||
- 8008:8008
|
||||
|
||||
volumes:
|
||||
py12306:
|
||||
dp_py12306:
|
||||
@@ -1,179 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 12306 账号
|
||||
USER_ACCOUNTS = [
|
||||
# 目前已支持仅查询,不下单,屏蔽掉下面的账号即可
|
||||
{
|
||||
'key': 0, # 如使用多个账号 key 不能重复
|
||||
'user_name': 'your user name',
|
||||
'password': 'your password'
|
||||
},
|
||||
# {
|
||||
# 'key': 'wangwu',
|
||||
# 'user_name': 'wangwu@qq.com',
|
||||
# 'password': 'wangwu'
|
||||
# }
|
||||
]
|
||||
|
||||
# 查询间隔(指每一个任务中每一个日期的间隔 / 单位秒)
|
||||
# 默认取间隔/2 到 间隔之间的随机数 如设置为 1 间隔则为 0.5 ~ 1 之间的随机数
|
||||
# 接受字典形式 格式: {'min': 0.5, 'max': 1}
|
||||
QUERY_INTERVAL = 1
|
||||
|
||||
# 用户心跳检测间隔 格式同上
|
||||
USER_HEARTBEAT_INTERVAL = 120
|
||||
|
||||
# 多线程查询
|
||||
QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任务会单独分配线程处理
|
||||
|
||||
# 打码平台账号
|
||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
|
||||
AUTO_CODE_ACCOUNT = {
|
||||
'user': 'your user name',
|
||||
'pwd': 'your password'
|
||||
}
|
||||
|
||||
# 语音验证码
|
||||
# 没找到比较好用的,现在用的这个是阿里云 API 市场上的,基本满足要求,价格也便宜
|
||||
# 购买成功后到控制台找到 APPCODE 放在下面就可以了
|
||||
# 地址:易源 https://market.aliyun.com/products/57126001/cmapi019902.html
|
||||
# 2019-01-18 更新
|
||||
# 增加新的服务商 鼎信 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.e27e7218KQttQS
|
||||
NOTIFICATION_BY_VOICE_CODE = 1 # 开启语音通知
|
||||
NOTIFICATION_VOICE_CODE_TYPE = 'dingxin' # 语音验证码服务商 可用项 dingxin yiyuan
|
||||
NOTIFICATION_API_APP_CODE = 'your app code'
|
||||
NOTIFICATION_VOICE_CODE_PHONE = 'your phone' # 接受通知的手机号
|
||||
|
||||
# 钉钉通知
|
||||
DINGTALK_ENABLED = 0
|
||||
DINGTALK_WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=your token'
|
||||
|
||||
# Telegram消息推送
|
||||
# 目前共有两个Bot:
|
||||
# 1:https://t.me/notificationme_bot
|
||||
# 2:https://t.me/RE_Link_Push_bot
|
||||
# 任选一个Bot,关注获取URL链接,如果没有回复则发送给Bot这条信息: /start
|
||||
# 将获取的URL填入下面对应位置
|
||||
# 注意:因为以上Bot都由他人公益提供,无法保证随时可用,如以上Bot都无法使用,请使用其他消息推送方式
|
||||
# Bot1来源:https://github.com/Fndroid/tg_push_bot
|
||||
# Bot2来源:https://szc.me/post/2.html
|
||||
TELEGRAM_ENABLED = 0
|
||||
TELEGRAM_BOT_API_URL = 'https://tgbot.lbyczf.com/sendMessage/:your_token'
|
||||
|
||||
# ServerChan 和 PushBear 微信消息推送
|
||||
# 使用说明
|
||||
# ServerChan http://sc.ftqq.com
|
||||
# PushBear http://pushbear.ftqq.com
|
||||
SERVERCHAN_ENABLED = 0
|
||||
SERVERCHAN_KEY = ''
|
||||
PUSHBEAR_ENABLED = 0
|
||||
PUSHBEAR_KEY = ''
|
||||
|
||||
# Bark 推送到ios设备
|
||||
# 参考 https://www.v2ex.com/t/467407
|
||||
BARK_ENABLED = 0
|
||||
BARK_PUSH_URL = 'https://api.day.app/:your_token'
|
||||
|
||||
# 输出日志到文件 (Docker 中不建议修改此组配置项)
|
||||
OUT_PUT_LOG_TO_FILE_ENABLED = 1
|
||||
OUT_PUT_LOG_TO_FILE_PATH = '/config/12306.log' # 日志目录
|
||||
RUNTIME_DIR = '/data/'
|
||||
QUERY_DATA_DIR = '/data/query/'
|
||||
USER_DATA_DIR = '/data/user/'
|
||||
|
||||
# 分布式集群配置
|
||||
CLUSTER_ENABLED = 0 # 集群状态
|
||||
NODE_IS_MASTER = 1 # 是否是主节点 同时只能启用 1 个主节点
|
||||
NODE_SLAVE_CAN_BE_MASTER = 1 # 主节点宕机后,子节点是否可以自动提升为主节点(建议打开)
|
||||
NODE_NAME = 'master' # 节点名称,不能重复
|
||||
REDIS_HOST = 'localhost' # Redis host
|
||||
REDIS_PORT = '6379' # Redis port
|
||||
REDIS_PASSWORD = '' # Redis 密码 没有可以留空
|
||||
|
||||
# 邮箱配置
|
||||
EMAIL_ENABLED = 0 # 是否开启邮件通知
|
||||
EMAIL_SENDER = 'sender@example.com' # 邮件发送者
|
||||
EMAIL_RECEIVER = 'receiver@example.com' # 邮件接受者 # 可以多个 [email1@gmail.com, email2@gmail.com]
|
||||
EMAIL_SERVER_HOST = 'localhost' # 邮件服务 host
|
||||
EMAIL_SERVER_USER = '' # 邮件服务登录用户名
|
||||
EMAIL_SERVER_PASSWORD = '' # 邮件服务登录密码
|
||||
|
||||
# Web 管理
|
||||
WEB_ENABLE = 1 # 是否打开 Web 管理
|
||||
WEB_USER = { # 登录信息
|
||||
'username': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
WEB_PORT = 8008 # 监听端口
|
||||
|
||||
# 是否开启 CDN 查询
|
||||
CDN_ENABLED = 0
|
||||
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
|
||||
|
||||
# 查询任务
|
||||
QUERY_JOBS = [
|
||||
{
|
||||
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
'account_key': 0, # 将会使用指定账号下单
|
||||
'left_dates': [ # 出发日期 :Array
|
||||
"2019-01-25",
|
||||
"2019-01-26",
|
||||
],
|
||||
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
||||
'left': '北京',
|
||||
'arrive': '深圳',
|
||||
},
|
||||
# # 多个车站示例 (建议添加多个,有时多买几站成功率会高一点)
|
||||
# 'stations': [{
|
||||
# 'left': '北京',
|
||||
# 'arrive': '深圳',
|
||||
# },{ # 多个车站示例
|
||||
# 'left': '北京',
|
||||
# 'arrive': '广州',
|
||||
# }],
|
||||
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
||||
"张三",
|
||||
"王五",
|
||||
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
||||
],
|
||||
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
||||
'seats': [ # 筛选座位 有先后顺序 :Array
|
||||
# 可用值: 特等座, 商务座, 一等座, 二等座, 软卧, 硬卧, 动卧, 软座, 硬座, 无座
|
||||
'硬卧',
|
||||
'硬座'
|
||||
],
|
||||
'train_numbers': [ # 筛选车次 可以为空,为空则所有车次都可以提交 如 [] 注意大小写需要保持一致
|
||||
"K356",
|
||||
"K1172",
|
||||
"K4184"
|
||||
],
|
||||
'except_train_numbers': [ # 筛选车次,排除车次 train_numbers 和 except_train_numbers 不可同时存在
|
||||
],
|
||||
'period': { # 筛选时间
|
||||
'from': '00:00',
|
||||
'to': '24:00'
|
||||
}
|
||||
|
||||
},
|
||||
# {
|
||||
# 'job_name': 'cd -> gz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
# 'account_key': 0, # 将会使用指定账号下单
|
||||
# 'left_dates': [
|
||||
# "2019-01-27",
|
||||
# "2019-01-28"
|
||||
# ],
|
||||
# 'stations': {
|
||||
# 'left': '成都',
|
||||
# 'arrive': '广州',
|
||||
# },
|
||||
# 'members': [
|
||||
# "小王",
|
||||
# ],
|
||||
# 'allow_less_member': 0,
|
||||
# 'seats': [
|
||||
# '硬卧',
|
||||
# ],
|
||||
# 'train_numbers': []
|
||||
# }
|
||||
]
|
||||
179
env.py.example
179
env.py.example
@@ -1,179 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 12306 账号
|
||||
USER_ACCOUNTS = [
|
||||
# 目前已支持仅查询,不下单,屏蔽掉下面的账号即可
|
||||
{
|
||||
'key': 0, # 如使用多个账号 key 不能重复
|
||||
'user_name': 'your user name',
|
||||
'password': 'your password'
|
||||
},
|
||||
# {
|
||||
# 'key': 'wangwu',
|
||||
# 'user_name': 'wangwu@qq.com',
|
||||
# 'password': 'wangwu'
|
||||
# }
|
||||
]
|
||||
|
||||
# 查询间隔(指每一个任务中每一个日期的间隔 / 单位秒)
|
||||
# 默认取间隔/2 到 间隔之间的随机数 如设置为 1 间隔则为 0.5 ~ 1 之间的随机数
|
||||
# 接受字典形式 格式: {'min': 0.5, 'max': 1}
|
||||
QUERY_INTERVAL = 1
|
||||
|
||||
# 用户心跳检测间隔 格式同上
|
||||
USER_HEARTBEAT_INTERVAL = 120
|
||||
|
||||
# 多线程查询
|
||||
QUERY_JOB_THREAD_ENABLED = 0 # 是否开启多线程查询,开启后第个任务会单独分配线程处理
|
||||
|
||||
# 打码平台账号
|
||||
# 目前只支持免费打码接口 和 若快打码,注册地址:http://www.ruokuai.com/login
|
||||
AUTO_CODE_PLATFORM = 'free' # 免费填写 free 若快 ruokuai # 免费打码无法保证持续可用,如失效请手动切换
|
||||
AUTO_CODE_ACCOUNT = { # 使用 free 可用省略
|
||||
'user': 'your user name',
|
||||
'pwd': 'your password'
|
||||
}
|
||||
|
||||
# 语音验证码
|
||||
# 没找到比较好用的,现在用的这个是阿里云 API 市场上的,基本满足要求,价格也便宜
|
||||
# 购买成功后到控制台找到 APPCODE 放在下面就可以了
|
||||
# 地址:易源 https://market.aliyun.com/products/57126001/cmapi019902.html
|
||||
# 2019-01-18 更新
|
||||
# 增加新的服务商 鼎信 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.e27e7218KQttQS
|
||||
NOTIFICATION_BY_VOICE_CODE = 1 # 开启语音通知
|
||||
NOTIFICATION_VOICE_CODE_TYPE = 'dingxin' # 语音验证码服务商 可用项 dingxin yiyuan
|
||||
NOTIFICATION_API_APP_CODE = 'your app code'
|
||||
NOTIFICATION_VOICE_CODE_PHONE = 'your phone' # 接受通知的手机号
|
||||
|
||||
# 钉钉通知
|
||||
# 使用说明 https://open-doc.dingtalk.com/docs/doc.htm?treeId=257&articleId=105735&docType=1
|
||||
DINGTALK_ENABLED = 0
|
||||
DINGTALK_WEBHOOK = 'https://oapi.dingtalk.com/robot/send?access_token=your token'
|
||||
|
||||
# Telegram消息推送
|
||||
# 目前共有两个Bot:
|
||||
# 1:https://t.me/notificationme_bot
|
||||
# 2:https://t.me/RE_Link_Push_bot
|
||||
# 任选一个Bot,关注获取URL链接,如果没有回复则发送给Bot这条信息: /start
|
||||
# 将获取的URL填入下面对应位置
|
||||
# 注意:因为以上Bot都由他人公益提供,无法保证随时可用,如以上Bot都无法使用,请使用其他消息推送方式
|
||||
# Bot1来源:https://github.com/Fndroid/tg_push_bot
|
||||
# Bot2来源:https://szc.me/post/2.html
|
||||
TELEGRAM_ENABLED = 0
|
||||
TELEGRAM_BOT_API_URL = 'https://tgbot.lbyczf.com/sendMessage/:your_token'
|
||||
|
||||
# ServerChan 和 PushBear 微信消息推送
|
||||
# 使用说明
|
||||
# ServerChan http://sc.ftqq.com
|
||||
# PushBear http://pushbear.ftqq.com
|
||||
SERVERCHAN_ENABLED = 0
|
||||
SERVERCHAN_KEY = ''
|
||||
PUSHBEAR_ENABLED = 0
|
||||
PUSHBEAR_KEY = ''
|
||||
|
||||
# Bark 推送到ios设备
|
||||
# 参考 https://www.v2ex.com/t/467407
|
||||
BARK_ENABLED = 0
|
||||
BARK_PUSH_URL = 'https://api.day.app/:your_token'
|
||||
|
||||
# 输出日志到文件
|
||||
# 默认设置不需要,related issue: https://github.com/pjialin/py12306/issues/319
|
||||
# 如需更改再 uncomment 掉下面这行
|
||||
# OUT_PUT_LOG_TO_FILE_ENABLED = 0
|
||||
OUT_PUT_LOG_TO_FILE_PATH = 'runtime/12306.log' # 日志目录
|
||||
|
||||
# 分布式集群配置
|
||||
CLUSTER_ENABLED = 0 # 集群状态
|
||||
NODE_IS_MASTER = 1 # 是否是主节点 同时只能启用 1 个主节点
|
||||
NODE_SLAVE_CAN_BE_MASTER = 1 # 主节点宕机后,子节点是否可以自动提升为主节点(建议打开)
|
||||
NODE_NAME = 'master' # 节点名称,不能重复
|
||||
REDIS_HOST = 'localhost' # Redis host
|
||||
REDIS_PORT = '6379' # Redis port
|
||||
REDIS_PASSWORD = '' # Redis 密码 没有可以留空
|
||||
|
||||
# 邮箱配置
|
||||
EMAIL_ENABLED = 0 # 是否开启邮件通知
|
||||
EMAIL_SENDER = 'sender@example.com' # 邮件发送者
|
||||
EMAIL_RECEIVER = 'receiver@example.com' # 邮件接受者 # 可以多个 [email1@gmail.com, email2@gmail.com]
|
||||
EMAIL_SERVER_HOST = 'localhost' # 邮件服务 host
|
||||
EMAIL_SERVER_USER = '' # 邮件服务登录用户名
|
||||
EMAIL_SERVER_PASSWORD = '' # 邮件服务登录密码
|
||||
|
||||
# Web 管理
|
||||
WEB_ENABLE = 1 # 是否打开 Web 管理
|
||||
WEB_USER = { # 登录信息
|
||||
'username': 'admin',
|
||||
'password': 'password'
|
||||
}
|
||||
WEB_PORT = 8008 # 监听端口
|
||||
|
||||
# 是否开启 CDN 查询
|
||||
CDN_ENABLED = 0
|
||||
CDN_CHECK_TIME_OUT = 1 # 检测单个 cdn 是否可用超时时间
|
||||
|
||||
# 查询任务
|
||||
QUERY_JOBS = [
|
||||
{
|
||||
# 'job_name': 'bj -> sz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
'account_key': 0, # 将会使用指定账号下单
|
||||
'left_dates': [ # 出发日期 :Array
|
||||
"2020-01-25",
|
||||
"2020-01-26",
|
||||
],
|
||||
'stations': { # 车站 支持多个车站同时查询 :Dict or :List
|
||||
'left': '北京',
|
||||
'arrive': '深圳',
|
||||
},
|
||||
# # 多个车站示例 (建议添加多个,有时多买几站成功率会高一点)
|
||||
# 'stations': [{
|
||||
# 'left': '北京',
|
||||
# 'arrive': '深圳',
|
||||
# },{ # 多个车站示例
|
||||
# 'left': '北京',
|
||||
# 'arrive': '广州',
|
||||
# }],
|
||||
'members': [ # 乘客姓名,会根据当前账号自动识别乘客类型 购买儿童票 设置两个相同的姓名即可,程序会自动识别 如 ['张三', '张三']
|
||||
"张三",
|
||||
#"*王五", #在姓名前加*表示学生购买成人票
|
||||
# 7, # 支持通过序号确定唯一乘客,序号查看可通过 python main.py -t 登录成功之后在 runtime/user/ 下找到对应的 用户名_passengers.json 文件,找到对应的 code 填入
|
||||
],
|
||||
'allow_less_member': 0, # 是否允许余票不足时提交部分乘客
|
||||
'seats': [ # 筛选座位 有先后顺序 :Array
|
||||
# 可用值: 特等座, 商务座, 一等座, 二等座, 软卧, 硬卧, 动卧, 软座, 硬座, 无座
|
||||
'硬卧',
|
||||
'硬座'
|
||||
],
|
||||
'train_numbers': [ # 筛选车次 可以为空,为空则所有车次都可以提交 如 [] 注意大小写需要保持一致
|
||||
"K356",
|
||||
"K1172",
|
||||
"K4184"
|
||||
],
|
||||
'except_train_numbers': [ # 筛选车次,排除车次 train_numbers 和 except_train_numbers 不可同时存在
|
||||
],
|
||||
'period': { # 筛选时间
|
||||
'from': '00:00',
|
||||
'to': '24:00'
|
||||
}
|
||||
|
||||
},
|
||||
# {
|
||||
# 'job_name': 'cd -> gz', # 任务名称,不填默认会以车站名命名,不可重复
|
||||
# 'account_key': 0, # 将会使用指定账号下单
|
||||
# 'left_dates': [
|
||||
# "2019-01-27",
|
||||
# "2019-01-28"
|
||||
# ],
|
||||
# 'stations': {
|
||||
# 'left': '成都',
|
||||
# 'arrive': '广州',
|
||||
# },
|
||||
# 'members': [
|
||||
# "小王",
|
||||
# ],
|
||||
# 'allow_less_member': 0,
|
||||
# 'seats': [
|
||||
# '硬卧',
|
||||
# ],
|
||||
# 'train_numbers': []
|
||||
# }
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 分布式子节点配置文件示例
|
||||
|
||||
# 分布式集群配置
|
||||
CLUSTER_ENABLED = 1 # 集群状态
|
||||
NODE_IS_MASTER = 0 # 是否是主节点
|
||||
NODE_NAME = 'slave 1' # 节点名称,不能重复
|
||||
REDIS_HOST = 'localhost' # Redis host
|
||||
REDIS_PORT = '6379' # Redis port
|
||||
REDIS_PASSWORD = '' # Redis 密码 没有可以留空
|
||||
|
||||
# 没了,其它配置会自动从主节点同步
|
||||
59
main.py
59
main.py
@@ -1,63 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
from py12306.app import *
|
||||
from py12306.helpers.cdn import Cdn
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
from py12306.web.web import Web
|
||||
sys.path.insert(0, 'py12306')
|
||||
|
||||
|
||||
def main():
|
||||
load_argvs()
|
||||
CommonLog.print_welcome()
|
||||
App.run()
|
||||
CommonLog.print_configs()
|
||||
App.did_start()
|
||||
|
||||
App.run_check()
|
||||
Query.check_before_run()
|
||||
|
||||
####### 运行任务
|
||||
Web.run()
|
||||
Cdn.run()
|
||||
User.run()
|
||||
Query.run()
|
||||
if not Const.IS_TEST:
|
||||
while True:
|
||||
sleep(10000)
|
||||
else:
|
||||
if Config().is_cluster_enabled(): stay_second(5) # 等待接受完通知
|
||||
CommonLog.print_test_complete()
|
||||
version_check()
|
||||
from app.app import App
|
||||
App.start_run_loop()
|
||||
|
||||
|
||||
def test():
|
||||
"""
|
||||
功能检查
|
||||
包含:
|
||||
账号密码验证 (打码)
|
||||
座位验证
|
||||
乘客验证
|
||||
语音验证码验证
|
||||
通知验证
|
||||
:return:
|
||||
"""
|
||||
Const.IS_TEST = True
|
||||
Config.OUT_PUT_LOG_TO_FILE_ENABLED = False
|
||||
if '--test-notification' in sys.argv or '-n' in sys.argv:
|
||||
Const.IS_TEST_NOTIFICATION = True
|
||||
pass
|
||||
|
||||
|
||||
def load_argvs():
|
||||
if '--test' in sys.argv or '-t' in sys.argv: test()
|
||||
config_index = None
|
||||
|
||||
if '--config' in sys.argv: config_index = sys.argv.index('--config')
|
||||
if '-c' in sys.argv: config_index = sys.argv.index('-c')
|
||||
if config_index:
|
||||
Config.CONFIG_FILE = sys.argv[config_index + 1:config_index + 2].pop()
|
||||
def version_check():
|
||||
if sys.version_info.major < 3 or sys.version_info.minor < 6:
|
||||
sys.exit('# Pleause use a python version that must equal to or greater than 3.6 #')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
169
py12306/app.py
169
py12306/app.py
@@ -1,169 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from py12306.helpers.func import *
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.notification import Notification
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.log.order_log import OrderLog
|
||||
|
||||
|
||||
def app_available_check():
|
||||
# return True # Debug
|
||||
if Config().IS_DEBUG:
|
||||
return True
|
||||
now = time_now()
|
||||
if (now.hour >= 23 and now.minute >= 30) or now.hour < 6:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_12306_IS_CLOSED.format(time_now())).flush()
|
||||
open_time = datetime.datetime(now.year, now.month, now.day, 6)
|
||||
if open_time < now:
|
||||
open_time += datetime.timedelta(1)
|
||||
sleep((open_time - now).seconds)
|
||||
return True
|
||||
|
||||
|
||||
@singleton
|
||||
class App:
|
||||
"""
|
||||
程序主类
|
||||
TODO 代码需要优化
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
self.register_sign()
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
Config().run()
|
||||
self.init_class()
|
||||
|
||||
@classmethod
|
||||
def did_start(cls):
|
||||
self = cls()
|
||||
from py12306.helpers.station import Station
|
||||
Station() # 防止多线程时初始化出现问题
|
||||
# if Config.is_cluster_enabled():
|
||||
# from py12306.cluster.cluster import Cluster
|
||||
# Cluster().run()
|
||||
|
||||
def init_class(self):
|
||||
from py12306.cluster.cluster import Cluster
|
||||
if Config.is_cluster_enabled():
|
||||
Cluster().run()
|
||||
|
||||
def register_sign(self):
|
||||
is_windows = os.name == 'nt'
|
||||
# if is_windows:
|
||||
signs = [signal.SIGINT, signal.SIGTERM]
|
||||
# else:
|
||||
# signs = [signal.SIGINT, signal.SIGHUP, signal.SIGTERM] # SIGHUP 会导致终端退出,程序也退出,暂时去掉
|
||||
for sign in signs:
|
||||
signal.signal(sign, self.handler_exit)
|
||||
|
||||
pass
|
||||
|
||||
def handler_exit(self, *args, **kwargs):
|
||||
"""
|
||||
程序退出
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if Config.is_cluster_enabled():
|
||||
from py12306.cluster.cluster import Cluster
|
||||
Cluster().left_cluster()
|
||||
|
||||
sys.exit()
|
||||
|
||||
@classmethod
|
||||
def check_auto_code(cls):
|
||||
if Config().AUTO_CODE_PLATFORM == 'free': return True
|
||||
if not Config().AUTO_CODE_ACCOUNT.get('user') or not Config().AUTO_CODE_ACCOUNT.get('pwd'):
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def check_user_account_is_empty(cls):
|
||||
if Config().USER_ACCOUNTS:
|
||||
for account in Config().USER_ACCOUNTS:
|
||||
if account:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def check_data_dir_exists():
|
||||
os.makedirs(Config().QUERY_DATA_DIR, exist_ok=True)
|
||||
os.makedirs(Config().USER_DATA_DIR, exist_ok=True)
|
||||
touch_file(Config().OUT_PUT_LOG_TO_FILE_PATH)
|
||||
|
||||
@classmethod
|
||||
def test_send_notifications(cls):
|
||||
if Config().NOTIFICATION_BY_VOICE_CODE: # 语音通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_VOICE_CODE).flush()
|
||||
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
|
||||
voice_content = {'left_station': '广州', 'arrive_station': '深圳', 'set_type': '硬座', 'orderno': 'E123542'}
|
||||
else:
|
||||
voice_content = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_VOICE_CODE_CONTENT.format('北京',
|
||||
'深圳')
|
||||
Notification.voice_code(Config().NOTIFICATION_VOICE_CODE_PHONE, '张三', voice_content)
|
||||
if Config().EMAIL_ENABLED: # 邮件通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_EMAIL).flush()
|
||||
Notification.send_email(Config().EMAIL_RECEIVER, '测试发送邮件', 'By py12306')
|
||||
|
||||
if Config().DINGTALK_ENABLED: # 钉钉通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_DINGTALK).flush()
|
||||
Notification.dingtalk_webhook('测试发送信息')
|
||||
|
||||
if Config().TELEGRAM_ENABLED: # Telegram通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_TELEGRAM).flush()
|
||||
Notification.send_to_telegram('测试发送信息')
|
||||
|
||||
if Config().SERVERCHAN_ENABLED: # ServerChan通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_SERVER_CHAN).flush()
|
||||
Notification.server_chan(Config().SERVERCHAN_KEY, '测试发送消息', 'By py12306')
|
||||
|
||||
if Config().PUSHBEAR_ENABLED: # PushBear通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_PUSH_BEAR).flush()
|
||||
Notification.push_bear(Config().PUSHBEAR_KEY, '测试发送消息', 'By py12306')
|
||||
|
||||
if Config().BARK_ENABLED: # Bark通知
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_TEST_SEND_PUSH_BARK).flush()
|
||||
Notification.push_bark('测试发送信息')
|
||||
|
||||
@classmethod
|
||||
def run_check(cls):
|
||||
"""
|
||||
待优化
|
||||
:return:
|
||||
"""
|
||||
cls.check_data_dir_exists()
|
||||
if not cls.check_user_account_is_empty():
|
||||
# CommonLog.add_quick_log(CommonLog.MESSAGE_CHECK_EMPTY_USER_ACCOUNT).flush(exit=True, publish=False) # 不填写用户则不自动下单
|
||||
if not cls.check_auto_code():
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CHECK_AUTO_CODE_FAIL).flush(exit=True, publish=False)
|
||||
if Const.IS_TEST_NOTIFICATION: cls.test_send_notifications()
|
||||
|
||||
|
||||
# Expand
|
||||
class Dict(dict):
|
||||
def get(self, key, default=None, sep='.'):
|
||||
keys = key.split(sep)
|
||||
for i, key in enumerate(keys):
|
||||
try:
|
||||
value = self[key]
|
||||
if len(keys[i + 1:]) and isinstance(value, Dict):
|
||||
return value.get(sep.join(keys[i + 1:]), default=default, sep=sep)
|
||||
return value
|
||||
except:
|
||||
return self.dict_to_dict(default)
|
||||
|
||||
def __getitem__(self, k):
|
||||
return self.dict_to_dict(super().__getitem__(k))
|
||||
|
||||
@staticmethod
|
||||
def dict_to_dict(value):
|
||||
return Dict(value) if isinstance(value, dict) else value
|
||||
8
py12306/app/__init__.py
Normal file
8
py12306/app/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import warnings
|
||||
|
||||
# Disable aiohttp annoying warn
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning, module='aiohttp')
|
||||
# disable unittest warnnings
|
||||
default_filterwarnings = warnings.filterwarnings
|
||||
warnings.simplefilter = lambda *args, **kwargs: None if args[0] == 'default' and not \
|
||||
kwargs else default_filterwarnings(*args, **kwargs)
|
||||
229
py12306/app/app.py
Normal file
229
py12306/app/app.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import aioredis
|
||||
import toml
|
||||
from aioredis import Redis
|
||||
from tortoise import Tortoise
|
||||
|
||||
from lib.exceptions import LoadConfigFailException
|
||||
from lib.hammer import *
|
||||
from lib.helper import SuperDict
|
||||
|
||||
|
||||
class ConfigInstance:
|
||||
APP_NAME = 'py12306'
|
||||
DEBUG = False
|
||||
IS_IN_TEST = False
|
||||
PROJECT_DIR = os.path.abspath(__file__ + '/../../../') + '/'
|
||||
DATA_DIR = PROJECT_DIR + 'data/'
|
||||
CONFIG_FILE = PROJECT_DIR + 'config.toml'
|
||||
CONFIG_TEST_FILE = PROJECT_DIR + 'config_test.toml'
|
||||
QUERY_AVAILABLE = False
|
||||
|
||||
# 默认请求超时
|
||||
REQUEST_TIME_OUT = 5
|
||||
# 用户心跳检测间隔
|
||||
USER_HEARTBEAT_INTERVAL = 120
|
||||
# USER_HEARTBEAT_INTERVAL = 10
|
||||
|
||||
# Config
|
||||
REDIS = {
|
||||
'enable': False,
|
||||
'host': '127.0.0.1', 'port': 6379, 'db': 0, 'password': None, 'decode_responses': True
|
||||
}
|
||||
DATABASE = {
|
||||
'db_url': f'sqlite://{DATA_DIR}db.sqlite3',
|
||||
}
|
||||
Notifaction = {}
|
||||
|
||||
_configs = {}
|
||||
|
||||
def load(self):
|
||||
""" Load configs from toml file """
|
||||
for arg in sys.argv:
|
||||
if arg.find('unittest') >= 0:
|
||||
self.IS_IN_TEST = True
|
||||
file_path = self.CONFIG_FILE
|
||||
if self.IS_IN_TEST and os.path.isfile(self.CONFIG_TEST_FILE):
|
||||
file_path = self.CONFIG_TEST_FILE
|
||||
configs = toml.load(file_path)
|
||||
self._configs = SuperDict(configs)
|
||||
self.REDIS.update(configs.get('redis', {}))
|
||||
db = configs.get('db', {})
|
||||
if db and db.get('engine') in ['mysql']:
|
||||
db['db_url'] = f"{db.get('engine')}://{db.get('user')}:{db.get('password')}@{db.get('host')}:{db.get('port')}/{db.get('database')}"
|
||||
self.DATABASE.update(configs.get('db', {}))
|
||||
self.DEBUG = self._configs.get('app.debug', self.DEBUG)
|
||||
self.Notifaction: dict = self._configs.get('notifaction', self.Notifaction)
|
||||
return self
|
||||
|
||||
@property
|
||||
def node_num(self) -> int:
|
||||
if not Config.QUERY_AVAILABLE:
|
||||
return 0
|
||||
# TODO
|
||||
return 1
|
||||
|
||||
@property
|
||||
def redis_able(self) -> bool:
|
||||
return self.REDIS.get('enable', False)
|
||||
|
||||
@property
|
||||
def proxy_able(self) -> bool:
|
||||
return self.get('proxy.enable', False)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._configs.get(key, default)
|
||||
|
||||
|
||||
class App:
|
||||
__instance = False
|
||||
|
||||
def __new__(cls) -> Any:
|
||||
if cls.__instance:
|
||||
return cls.__instance
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.__class__.__instance = self
|
||||
self.__features = []
|
||||
|
||||
def start_run_loop(self):
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.__run_loop())
|
||||
|
||||
async def __run_loop(self):
|
||||
self.print_welcome()
|
||||
await self.init_db()
|
||||
await self.__load_data()
|
||||
await self.__run_query_loop()
|
||||
ret = await asyncio.wait(self.__features, return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
async def __run_query_loop(self):
|
||||
""" 12306 查询相关
|
||||
) 服务时间检测
|
||||
"""
|
||||
from app.user import TrainUserManager
|
||||
from app.query import QueryTicketManager
|
||||
from app.order import OrderTicketManager
|
||||
self.__features.append(self.__service_check_loop())
|
||||
self.__features.append(TrainUserManager.share().run())
|
||||
self.__features.append(QueryTicketManager.share().run())
|
||||
self.__features.append(OrderTicketManager.share().run())
|
||||
|
||||
async def __service_check_loop(self):
|
||||
last_check = None
|
||||
while True:
|
||||
if not Config.DEBUG and not self.check_12306_service_time():
|
||||
if not last_check or (datetime.datetime.now() - last_check).seconds > 3600:
|
||||
Logger.info(f'程序运行中,当前时间: {datetime.datetime.now()} | 12306 休息时间,程序将在明天早上 6 点自动运行')
|
||||
last_check = datetime.datetime.now()
|
||||
Config.QUERY_AVAILABLE = False
|
||||
else:
|
||||
Config.QUERY_AVAILABLE = True
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def init_db(self):
|
||||
await Tortoise.init(
|
||||
db_url=Config.DATABASE['db_url'],
|
||||
modules={'models': ['app.models']})
|
||||
# Generate the schema
|
||||
await Tortoise.generate_schemas()
|
||||
|
||||
def check_12306_service_time(self):
|
||||
""" 服务时间检测 """
|
||||
now = datetime.datetime.now()
|
||||
if (now.hour >= 23 and now.minute >= 30) or now.hour < 6:
|
||||
return False
|
||||
return True
|
||||
|
||||
def print_welcome(self):
|
||||
Logger.info('######## py12306 购票助手,本程序为开源工具,请勿用于商业用途 ########')
|
||||
if Config.DEBUG:
|
||||
Logger.info('Debug 模式已启用')
|
||||
|
||||
async def __load_data(self):
|
||||
""" 加载配置数据
|
||||
) 加载用户
|
||||
) 加载查询
|
||||
"""
|
||||
from app.models import User
|
||||
from app.models import QueryJob
|
||||
try:
|
||||
users = Config.get('user', [])
|
||||
for _user in users:
|
||||
await User.load_from_config(_user)
|
||||
querys = Config.get('query', [])
|
||||
for _query in querys:
|
||||
await QueryJob.load_from_config(_query)
|
||||
except LoadConfigFailException as e:
|
||||
Logger.error(f'配置验证失败,{e.msg}')
|
||||
await self.exit()
|
||||
|
||||
async def exit(self, msg: str = ''):
|
||||
await Tortoise.close_connections()
|
||||
Logger.info('# 程序已退出 #')
|
||||
sys.exit(msg)
|
||||
|
||||
|
||||
# set up logger
|
||||
def __set_up_logger() -> logging.Logger:
|
||||
logger = logging.getLogger(Config.APP_NAME)
|
||||
logger.setLevel('DEBUG' if Config.DEBUG else 'INFO')
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
|
||||
def __load_event():
|
||||
engine = AsyncioEventEngine
|
||||
if Config.redis_able:
|
||||
engine = RedisEventEngine
|
||||
return EventHammer(engine())
|
||||
|
||||
|
||||
def __load_cache():
|
||||
engine = AsyncioCacheEngine
|
||||
if Config.redis_able:
|
||||
engine = RedisCacheEngine
|
||||
return CacheHammer(engine())
|
||||
|
||||
|
||||
def __load_redis() -> Redis:
|
||||
if Config.redis_able:
|
||||
addrss = f"redis://{Config.REDIS['host']}:{Config.REDIS['port']}"
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
aioredis.create_redis_pool(addrss, db=Config.REDIS['db'], password=Config.REDIS['password']))
|
||||
return None
|
||||
|
||||
|
||||
def __load_notifaction():
|
||||
from app import notification as nt
|
||||
center = nt.NotificationCenter()
|
||||
conf = Config.Notifaction
|
||||
if conf.get('ding_talk.enable'):
|
||||
center.add_backend(nt.DingTalkNotifaction(conf['ding_talk']))
|
||||
if conf.get('bark.enable'):
|
||||
center.add_backend(nt.BarkNotifaction(conf['bark']))
|
||||
if conf.get('email.enable'):
|
||||
center.add_backend(nt.EmailNotifaction(conf['email']))
|
||||
if conf.get('server_chan.enable'):
|
||||
center.add_backend(nt.ServerChanNotifaction(conf['server_chan']))
|
||||
if conf.get('ding_xing_voice.enable'):
|
||||
center.add_backend(nt.DingXinVoiceNotifaction(conf['ding_xing_voice']))
|
||||
return center
|
||||
|
||||
|
||||
# load config
|
||||
Config = ConfigInstance().load()
|
||||
Redis = __load_redis()
|
||||
Logger = __set_up_logger()
|
||||
Event = __load_event()
|
||||
Cache = __load_cache()
|
||||
Notification = __load_notifaction()
|
||||
App = App()
|
||||
349
py12306/app/models.py
Normal file
349
py12306/app/models.py
Normal file
@@ -0,0 +1,349 @@
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
import urllib
|
||||
from typing import List
|
||||
|
||||
from tortoise import Model, fields
|
||||
|
||||
from lib.exceptions import LoadConfigFailException
|
||||
from lib.helper import json_friendly_loads, json_friendly_dumps, StationHelper
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
if hasattr(self, 'hash_id'):
|
||||
self.hash_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
async def refresh_from_db(self):
|
||||
new = await self.__class__.filter(id=self.id).first()
|
||||
self.__dict__.update(new.__dict__)
|
||||
|
||||
@classmethod
|
||||
async def get_or_create_instance(cls, **kwargs):
|
||||
ret = await cls.filter(**kwargs).first()
|
||||
if not ret:
|
||||
ret = cls()
|
||||
for k, v in kwargs.items():
|
||||
setattr(ret, k, v)
|
||||
return ret
|
||||
|
||||
|
||||
class FriendlyJSONField(fields.JSONField):
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(encoder=json_friendly_dumps, decoder=json_friendly_loads, **kwargs)
|
||||
|
||||
|
||||
class QueryJob(TimestampMixin, BaseModel):
|
||||
""" 查询任务 """
|
||||
|
||||
class Status:
|
||||
Normal = 'normal'
|
||||
WaitVerify = 'wait_verify'
|
||||
Finished = 'finished'
|
||||
Error = 'error'
|
||||
|
||||
hash_id = fields.CharField(max_length=16, default='', index=True, description='Hash')
|
||||
user = fields.ForeignKeyField('models.User', on_delete=fields.SET_NULL, null=True)
|
||||
name = fields.CharField(max_length=255, default='', description='任务名称')
|
||||
left_dates = FriendlyJSONField(default=[], description='出发日期')
|
||||
left_date = fields.DateField(default=None, null=True, description='当前出发日期')
|
||||
stations = FriendlyJSONField(default=[], description='查询车站')
|
||||
left_station = fields.CharField(max_length=255, default='', description='出发地')
|
||||
arrive_station = fields.CharField(max_length=255, default='', description='到达地')
|
||||
left_periods = FriendlyJSONField(default={}, description='出发时间')
|
||||
allow_train_numbers = FriendlyJSONField(default=[], description='筛选车次')
|
||||
execpt_train_numbers = FriendlyJSONField(default=[], description='筛选车次(反')
|
||||
allow_seats = FriendlyJSONField(default=[], description='筛选座位')
|
||||
members = FriendlyJSONField(default=[], description='乘车人员')
|
||||
member_num = fields.SmallIntField(default=0, description='人员数量')
|
||||
query_num = fields.IntField(default=0, description='查询次数')
|
||||
less_member = fields.BooleanField(default=False, description='提交部分乘客')
|
||||
status = fields.CharField(max_length=20, default=Status.WaitVerify, index=True, description='任务状态')
|
||||
enable = fields.BooleanField(default=True, description='启用状态')
|
||||
passengers = FriendlyJSONField(default={}, description='乘客列表')
|
||||
last_process_at = fields.DatetimeField(default=None, null=True, description='最后一次执行')
|
||||
last_error = fields.CharField(default='', max_length=500, description='最后一次错误')
|
||||
|
||||
class Meta:
|
||||
table = 'query_jobs'
|
||||
|
||||
@property
|
||||
def is_queryable(self) -> bool:
|
||||
""" 验证任务是否可查询 有一个可用即可"""
|
||||
for left_date in self.left_dates:
|
||||
if left_date < datetime.datetime.now().date():
|
||||
continue
|
||||
if left_date > (datetime.datetime.now().date() + datetime.timedelta(days=31)):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_is_queryable(self) -> bool:
|
||||
""" 验证当前任务是否可查询"""
|
||||
if self.left_date < datetime.datetime.now().date():
|
||||
return False
|
||||
if self.left_date > (datetime.datetime.now().date() + datetime.timedelta(days=30)):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def query_num_next(self) -> int:
|
||||
self.query_num += 1
|
||||
return self.query_num
|
||||
|
||||
@classmethod
|
||||
def filter_available(cls):
|
||||
return cls.filter(enable=True, status=QueryJob.Status.Normal)
|
||||
|
||||
@property
|
||||
def name_text(self):
|
||||
""" 任务名称 """
|
||||
return f'ID {self.id} {self.name or self.route_time_text}'.strip()
|
||||
|
||||
@property
|
||||
def route_text(self):
|
||||
""" 行程文本 北京 - 深圳,北京 — 广州 """
|
||||
return ','.join([f'{station[0]} - {station[1]}' for station in self.stations])
|
||||
|
||||
@property
|
||||
def current_route_text(self) -> str:
|
||||
""" 行程文本 北京 - 深圳 """
|
||||
return f'{self.left_station} - {self.arrive_station}'
|
||||
|
||||
@property
|
||||
def left_time_text(self):
|
||||
""" 出发时间文本 2020-01-05,2020-01-06 """
|
||||
return ','.join([str(left_date) for left_date in self.left_dates])
|
||||
|
||||
@property
|
||||
def route_time_text(self):
|
||||
""" 路程与时间信息 """
|
||||
return f'{self.route_text} 出发时间 {self.left_time_text}'
|
||||
|
||||
@property
|
||||
def left_station_id(self):
|
||||
return StationHelper.id_by_cn(self.left_station)
|
||||
|
||||
@property
|
||||
def arrive_station_id(self):
|
||||
return StationHelper.id_by_cn(self.arrive_station)
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self.enable and self.status == self.Status.Normal
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
return self.last_process_at and (datetime.datetime.now() - self.last_process_at).seconds < 30
|
||||
|
||||
async def update_last_process_at(self) -> datetime.datetime:
|
||||
self.last_process_at = datetime.datetime.now()
|
||||
await self.save()
|
||||
return self.last_process_at
|
||||
|
||||
@classmethod
|
||||
async def load_from_config(cls, config: dict):
|
||||
if not config.get('id'):
|
||||
raise LoadConfigFailException('未指定查询任务 ID')
|
||||
user, user_id = None, config.get('user_id')
|
||||
if user_id:
|
||||
user = await User.filter(id=user_id).first()
|
||||
if not user:
|
||||
raise LoadConfigFailException(f'未找到查询订单关联用户 id {user_id}')
|
||||
else:
|
||||
user_id = None
|
||||
query = await cls.get_or_create_instance(id=config['id'])
|
||||
stations = config.get('stations', [])
|
||||
members = config.get('members', [])
|
||||
periods = config.get('periods', [])
|
||||
# 验证时间筛选
|
||||
if periods and len(periods) is not 2:
|
||||
raise LoadConfigFailException(f'乘车时间区间验证失败 {periods}')
|
||||
# 验证查询车站
|
||||
if not len(stations) or len(stations) % 2 is not 0:
|
||||
raise LoadConfigFailException('查询车站配置验证失败')
|
||||
stations = [stations[i:i + 2] for i in range(0, len(stations), 2)]
|
||||
for station in stations:
|
||||
if not StationHelper.id_by_cn(station[0]) or not StationHelper.id_by_cn(station[1]):
|
||||
raise LoadConfigFailException(f'未找到该车站 {station}')
|
||||
if not len(members):
|
||||
raise LoadConfigFailException('乘车人员验证失败')
|
||||
# 验证乘车时间
|
||||
left_dates_str_list = config.get('left_dates', [])
|
||||
if not left_dates_str_list:
|
||||
raise LoadConfigFailException('乘车日期验证失败')
|
||||
left_dates = []
|
||||
for left_date_str in left_dates_str_list:
|
||||
try:
|
||||
left_dates.append(datetime.datetime.strptime(left_date_str, '%Y-%m-%d').date())
|
||||
except Exception:
|
||||
raise LoadConfigFailException(f'乘车日期格式验证失败 {left_date_str}')
|
||||
|
||||
query.enable = config.get('enable', True)
|
||||
if query.user_id is not user_id: # 恢复为待验证状态
|
||||
query.status = query.Status.WaitVerify
|
||||
query.user_id = user_id or query.user_id
|
||||
query.allow_seats = config.get('seats', [])
|
||||
query.allow_train_numbers = list(map(str.upper, config.get('train_numbers', [])))
|
||||
query.execpt_train_numbers = list(
|
||||
map(str.upper, config.get('except_train_numbers', [])))
|
||||
query.left_periods = periods or query.left_periods
|
||||
query.members = members
|
||||
query.member_num = len(query.members)
|
||||
query.left_dates = left_dates
|
||||
query.stations = stations
|
||||
await query.save()
|
||||
return query
|
||||
|
||||
|
||||
class Ticket(TimestampMixin, BaseModel):
|
||||
""" Ticket """
|
||||
hash_id = fields.CharField(max_length=16, default='', index=True, description='Hash')
|
||||
left_date = fields.DateField(default=None, null=True)
|
||||
ticket_num = fields.CharField(default='', max_length=255, description='余票数量')
|
||||
train_number = fields.CharField(default='', max_length=255)
|
||||
train_no = fields.CharField(default='', max_length=255)
|
||||
left_station = fields.CharField(default='', max_length=255)
|
||||
arrive_station = fields.CharField(default='', max_length=255)
|
||||
order_text = fields.CharField(default='', max_length=255)
|
||||
secret_str = fields.CharField(default='', max_length=1000)
|
||||
left_time = fields.CharField(default='', max_length=255)
|
||||
arrive_time = fields.CharField(default='', max_length=255)
|
||||
# { 'name': seat, 'id': seat_id, 'raw': raw, 'order_id': TrainSeat.order_id[seat] }
|
||||
available_seat = FriendlyJSONField(default=[], description='可用座位')
|
||||
member_num_take = fields.SmallIntField(default=0, description='实际人员数量')
|
||||
raw = FriendlyJSONField(default={})
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
class Meta:
|
||||
table = 'tickets'
|
||||
|
||||
@property
|
||||
def left_date_order(self):
|
||||
return self.left_date.strftime('%Y-%m-%d')
|
||||
|
||||
@property
|
||||
def secret_str_unquote(self) -> str:
|
||||
return urllib.parse.unquote(self.secret_str)
|
||||
|
||||
@classmethod
|
||||
def parse_tickets_text(cls, tickts: List[str]):
|
||||
ret = []
|
||||
for tickt_str in tickts:
|
||||
ticket = cls.from_ticket_text(tickt_str)
|
||||
ret.append(ticket)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def baby(self):
|
||||
""" 小黑屋 ID """
|
||||
return f"{self.left_date}_{self.train_number}_{self.available_seat.get('id')}"
|
||||
|
||||
@property
|
||||
def route_text(self):
|
||||
""" 行程文本 北京 - 深圳 """
|
||||
return f'{self.left_station} - {self.arrive_station}'
|
||||
|
||||
@property
|
||||
def detail_text(self):
|
||||
""" 车次详细信息 车次 G335 时间 23:15 - 14:25 硬卧 余票 10 """
|
||||
return f"车次 {self.train_number} 时间 {self.left_date} {self.left_time} - {self.arrive_time} {self.available_seat.get('name')} 余票 {self.ticket_num}"
|
||||
|
||||
@property
|
||||
def dark_room_text(self):
|
||||
""" 小黑屋信息 车次 G335 时间 2020-01-20 硬卧 """
|
||||
return f"车次 {self.train_number} 时间 {self.left_date} {self.available_seat.get('name')}"
|
||||
|
||||
@classmethod
|
||||
def from_ticket_text(cls, ticket_str):
|
||||
info = ticket_str.split('|')
|
||||
ticket = Ticket()
|
||||
ticket.left_date = datetime.datetime.strptime(info[13], '%Y%m%d').date()
|
||||
ticket.train_no = info[2]
|
||||
ticket.ticket_num = info[11]
|
||||
ticket.train_number = info[3]
|
||||
ticket.left_station = info[6]
|
||||
ticket.arrive_station = info[7]
|
||||
ticket.order_text = info[1]
|
||||
ticket.secret_str = info[0]
|
||||
ticket.left_time = info[8]
|
||||
ticket.arrive_time = info[9]
|
||||
ticket.raw = info
|
||||
return ticket
|
||||
|
||||
|
||||
class User(TimestampMixin, BaseModel):
|
||||
""" 12306 用户 """
|
||||
|
||||
user_id = fields.CharField(default='', max_length=50, description='12306 ID')
|
||||
name = fields.CharField(default='', max_length=255, description='用户名')
|
||||
password = fields.CharField(default='', max_length=255, description='密码')
|
||||
real_name = fields.CharField(default='', max_length=255, description='姓名')
|
||||
last_heartbeat = fields.DatetimeField(default=None, null=True, description='上次心跳')
|
||||
last_cookies = fields.TextField(default=None, null=True, description='上次 Cookie')
|
||||
passengers = FriendlyJSONField(default={}, description='乘客列表')
|
||||
enable = fields.BooleanField(default=True, description='启用状态')
|
||||
last_process_at = fields.DatetimeField(default=None, null=True, description='最后一次执行')
|
||||
|
||||
class Meta:
|
||||
table = 'users'
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
return self.last_process_at and (datetime.datetime.now() - self.last_process_at).seconds < 60
|
||||
|
||||
@property
|
||||
def name_text(self) -> str:
|
||||
""" 任务名称 """
|
||||
return f'ID {self.id} {self.user_id}'
|
||||
|
||||
async def update_last_process_at(self) -> datetime.datetime:
|
||||
self.last_process_at = datetime.datetime.now()
|
||||
await self.save()
|
||||
return self.last_process_at
|
||||
|
||||
@classmethod
|
||||
async def load_from_config(cls, config: dict):
|
||||
if not config.get('id'):
|
||||
raise LoadConfigFailException('未指定用户 ID')
|
||||
user = await cls.get_or_create_instance(id=config['id'])
|
||||
user.enable = config.get('enable', True)
|
||||
user.name = config.get('name', '')
|
||||
user.password = config.get('password')
|
||||
await user.save()
|
||||
return user
|
||||
|
||||
|
||||
class Order(TimestampMixin, BaseModel):
|
||||
""" 订单 """
|
||||
|
||||
class Status:
|
||||
Wait = 'wait'
|
||||
DarkRoom = 'dark_room' # 小黑屋
|
||||
Error = 'error'
|
||||
Success = 'success'
|
||||
|
||||
user = fields.ForeignKeyField('models.User', on_delete=fields.SET_NULL, null=True)
|
||||
query_job = fields.ForeignKeyField('models.QueryJob', on_delete=fields.SET_NULL, null=True)
|
||||
ticket = fields.ForeignKeyField('models.Ticket', on_delete=fields.SET_NULL, null=True)
|
||||
status = fields.CharField(max_length=20, default=Status.Wait, index=True, description='任务状态')
|
||||
last_error = fields.CharField(default='', max_length=500, description='最后一次错误')
|
||||
|
||||
class Meta:
|
||||
table = 'orders'
|
||||
193
py12306/app/notification.py
Normal file
193
py12306/app/notification.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import smtplib
|
||||
from abc import ABC, abstractmethod
|
||||
from email.message import EmailMessage
|
||||
from typing import List
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from app.app import Logger
|
||||
from app.models import Order, Ticket
|
||||
from lib.helper import retry
|
||||
from lib.request import Session
|
||||
|
||||
|
||||
class NotifactionMessage:
|
||||
|
||||
def __init__(self, title: str = '', message: str = '', extra: dict = None) -> None:
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.extra = extra or {}
|
||||
|
||||
def to_str(self):
|
||||
return f"{self.title}\n\n{self.message}"
|
||||
|
||||
|
||||
class NotifactionAbstract(ABC):
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
super().__init__()
|
||||
self.session = Session.share()
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class NotificationCenter:
|
||||
""" 通知类 """
|
||||
|
||||
def __init__(self):
|
||||
self.backends: List[NotifactionAbstract] = []
|
||||
|
||||
def add_backend(self, backend: NotifactionAbstract):
|
||||
self.backends.append(backend)
|
||||
|
||||
async def order_success_notifation(self, order: Order):
|
||||
body = f"请及时登录12306账号[{order.user.name}],打开 '未完成订单',在30分钟内完成支付!" \
|
||||
# f"\n\n车次信息: {} {}[{}] -> {}[{}],乘车日期 {},席位:{},乘车人:{}"
|
||||
# TODO
|
||||
message = NotifactionMessage(title='车票购买成功!', message=body)
|
||||
message.extra = {
|
||||
'name': order.user.real_name,
|
||||
'left_station': '广州',
|
||||
'arrive_station': '深圳',
|
||||
'set_type': '硬座',
|
||||
'orderno': 'E123542'
|
||||
}
|
||||
await self.send_message(message)
|
||||
|
||||
async def ticket_available_notifation(self, ticket: Ticket):
|
||||
title = f'余票监控通知 {ticket.route_text}'
|
||||
body = f'{ticket.detail_text}'
|
||||
await self.send_message(NotifactionMessage(title=title, message=body))
|
||||
|
||||
async def send_message(self, message: NotifactionMessage):
|
||||
for backend in self.backends:
|
||||
await backend.send(message)
|
||||
|
||||
|
||||
class DingTalkNotifaction(NotifactionAbstract):
|
||||
""" 钉钉 """
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
api_url = f"https://oapi.dingtalk.com/robot/send?access_token={self.config.get('access_token')}"
|
||||
data = {'msgtype': 'text', 'text': {'content': message.to_str()}}
|
||||
response = await self.session.request('POST', api_url, json=data)
|
||||
result = response.json()
|
||||
if result.get('errcode') == 0:
|
||||
Logger.info('钉钉 推送成功')
|
||||
return True
|
||||
Logger.info(f"钉钉 推送失败,{result.get('errmsg')}")
|
||||
return False
|
||||
|
||||
|
||||
class BarkNotifaction(NotifactionAbstract):
|
||||
""" Bark """
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
api_url = f"{self.config.get('push_url')}/{message.to_str()}"
|
||||
response = await self.session.request('GET', api_url)
|
||||
result = response.json()
|
||||
if result.get('code') == 200:
|
||||
Logger.info('Bark 推送成功')
|
||||
return True
|
||||
Logger.info(f"Bark 推送失败,{result.get('message')}")
|
||||
return False
|
||||
|
||||
|
||||
class EmailNotifaction(NotifactionAbstract):
|
||||
""" Email """
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
to = self.config.get('to')
|
||||
if not to:
|
||||
Logger.warning('未配置邮件接受用户')
|
||||
return False
|
||||
to = to if isinstance(to, list) else [to]
|
||||
email_message = EmailMessage()
|
||||
email_message['Subject'] = message.title
|
||||
email_message['From'] = self.config.get('sender', '')
|
||||
email_message['To'] = to
|
||||
email_message.set_content(message.message)
|
||||
try:
|
||||
server = smtplib.SMTP(self.config.get('host'))
|
||||
server.ehlo()
|
||||
server.starttls()
|
||||
server.login(self.config.get('user', ''), self.config.get('password', ''))
|
||||
server.send_message(email_message)
|
||||
server.quit()
|
||||
Logger.info('邮件发送成功,请检查收件箱')
|
||||
return True
|
||||
except Exception as e:
|
||||
Logger.error(f'邮件发送失败,请手动检查配置,错误原因 {e}')
|
||||
return False
|
||||
|
||||
|
||||
class ServerChanNotifaction(NotifactionAbstract):
|
||||
""" ServerChan
|
||||
http://sc.ftqq.com/3.version
|
||||
"""
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
api_url = f"https://sc.ftqq.com/{self.config.get('sckey')}.send"
|
||||
data = {'text': message.title, 'desp': message.message}
|
||||
response = await self.session.request('POST', api_url, data=data)
|
||||
result = response.json()
|
||||
if result.get('errno') == 0:
|
||||
Logger.info('ServerChan 推送成功')
|
||||
return True
|
||||
Logger.info(f"ServerChan 推送失败,{result.get('error_message', result.get('errmsg'))}")
|
||||
return False
|
||||
|
||||
|
||||
class PushBearNotifaction(NotifactionAbstract):
|
||||
""" PushBear # 已失效
|
||||
http://pushbear.ftqq.com/admin/#/
|
||||
"""
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
api_url = f"https://pushbear.ftqq.com/sub?sendkey={self.config.get('sendkey')}&"
|
||||
querys = {'text': message.title, 'desp': message.message}
|
||||
response = await self.session.request('GET', api_url + urlencode(querys))
|
||||
result = response.json()
|
||||
if result.get('code') == 0:
|
||||
Logger.info('PushBear 推送成功')
|
||||
return True
|
||||
Logger.info(f"PushBear 推送失败,{result.get('errmsg')}")
|
||||
return False
|
||||
|
||||
|
||||
class DingXinVoiceNotifaction(NotifactionAbstract):
|
||||
""" 发送语音验证码 ( 鼎信 )
|
||||
购买地址 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.51547218rkAXxy
|
||||
"""
|
||||
|
||||
@retry
|
||||
async def send(self, message: NotifactionMessage) -> bool:
|
||||
phone = self.config.get('phone')
|
||||
if not phone:
|
||||
Logger.warning('未配置语音通知手机号')
|
||||
return False
|
||||
api_url = f"http://yuyin2.market.alicloudapi.com/dx/voice_notice"
|
||||
headers = {'Authorization': f"APPCODE {self.config.get('appcode')}"}
|
||||
data = {
|
||||
'tpl_id': 'TP1901174',
|
||||
'phone': phone,
|
||||
'param': f"name:{message.extra.get('name', '')},job_name:{message.extra.get('left_station', '')}"
|
||||
f"到{message.extra.get('arrive_station', '')}{message.extra.get('set_name', '')},"
|
||||
f"orderno:{message.extra.get('orderno', '')}"}
|
||||
response = await self.session.request('POST', api_url, headers=headers, data=data)
|
||||
result = response.json()
|
||||
if result.get('return_code') in [400, 401, 403]:
|
||||
Logger.error('语音消息发送失败,请检查 appcode 是否填写正确或 套餐余额是否充足')
|
||||
elif result.get('return_code') == '00000':
|
||||
Logger.info(f"语音消息发送成功!")
|
||||
return True
|
||||
Logger.info(f"语音消息发送失败,{result.get('return_code')}")
|
||||
return False
|
||||
407
py12306/app/order.py
Normal file
407
py12306/app/order.py
Normal file
@@ -0,0 +1,407 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
|
||||
from app.app import Event, Cache, Notification
|
||||
from app.app import Logger, Config
|
||||
from app.models import QueryJob, User, Order, Ticket
|
||||
from app.notification import NotifactionMessage
|
||||
from app.user import TrainUser, TrainUserManager
|
||||
from lib.exceptions import RetryException
|
||||
from lib.helper import StationHelper, UserTypeHelper, TrainSeat, TaskManager, retry
|
||||
|
||||
|
||||
class OrderTicketManager(TaskManager):
|
||||
|
||||
async def run(self):
|
||||
self.fuatures.append(asyncio.ensure_future(self.subscribe_loop()))
|
||||
await self.wait()
|
||||
|
||||
@property
|
||||
async def task_total(self) -> int:
|
||||
return 0
|
||||
|
||||
async def subscribe_loop(self):
|
||||
while True:
|
||||
event = await Event.subscribe()
|
||||
if event.name == Event.EVENT_ORDER_TICKET:
|
||||
await self.handle_order_task(event.data)
|
||||
|
||||
async def handle_order_task(self, data: dict):
|
||||
""" 处理订单任务 """
|
||||
try:
|
||||
user_id = data['user_id']
|
||||
train_user: TrainUser = TrainUserManager.share().get_task(user_id)
|
||||
if not train_user or not train_user.is_ready:
|
||||
Logger.warning(f'用户 ID {user_id} 不可用,已跳过该下单任务')
|
||||
return
|
||||
# make order model
|
||||
query_job = QueryJob(**data['query_job'])
|
||||
ticket = Ticket(**data['ticket'])
|
||||
|
||||
Logger.info(f'# 任务 {query_job.current_route_text} 开始下单 #')
|
||||
future = asyncio.ensure_future(OrderTicket(query_job, ticket, train_user).order())
|
||||
self.fuatures.append(future)
|
||||
except Exception as e:
|
||||
Logger.error(f'订单任务解析失败,{e}')
|
||||
|
||||
|
||||
class OrderTicket:
|
||||
""" 处理下单 """
|
||||
|
||||
def __init__(self, query_job: QueryJob, ticket: Ticket, train_user: TrainUser):
|
||||
self.session = train_user.session
|
||||
self.query: QueryJob = query_job
|
||||
self.user: User = train_user.user
|
||||
self.ticket: Ticket = ticket
|
||||
self.train_user = train_user
|
||||
self._order = Order()
|
||||
# Init page
|
||||
self.global_repeat_submit_token = None
|
||||
self.ticket_info_for_passenger_form = None
|
||||
self.order_request_dto = None
|
||||
# Config
|
||||
self.max_queue_wait = 60 * 5
|
||||
self._passenger_ticket_str = ''
|
||||
self._old_passenger_str = ''
|
||||
self._order_id = ''
|
||||
|
||||
async def order(self):
|
||||
""" 开始下单 """
|
||||
try:
|
||||
while True:
|
||||
if self.train_user.is_ordering:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
if await Cache.in_dark_room(self.ticket.baby):
|
||||
self._order.status = self._order.Status.DarkRoom
|
||||
return
|
||||
break
|
||||
self.train_user.is_ordering = True
|
||||
return await self.normal_order()
|
||||
except Exception as err:
|
||||
self._order.status = self._order.Status.Error
|
||||
self._order.last_error = str(err)
|
||||
finally:
|
||||
self.train_user.is_ordering = False
|
||||
# 更新为正确的关联信息
|
||||
self.user = self.user
|
||||
self._order.query_job = await QueryJob.filter(hash_id=self.query.hash_id).first()
|
||||
if self._order_id and self._order.query_job: # 更新订单状态
|
||||
self._order.query_job.status = self._order.query_job.Status.Finished
|
||||
await self._order.query_job.save()
|
||||
self._order.status = self._order.Status.Success
|
||||
self._order.ticket = await Ticket.filter(hash_id=self.ticket.hash_id).first()
|
||||
await self._order.save()
|
||||
|
||||
async def normal_order(self):
|
||||
""" 下单
|
||||
) 提交下单请求
|
||||
"""
|
||||
try:
|
||||
order_request_res = await self.submit_order_request()
|
||||
if not order_request_res:
|
||||
return
|
||||
if not await self.request_init_dc_page():
|
||||
return
|
||||
if not await self.check_order_info():
|
||||
return
|
||||
if not await self.get_queue_count():
|
||||
return
|
||||
if not await self.confirm_order_queue():
|
||||
return
|
||||
order_status = await self.wait_order_queue()
|
||||
if order_status and self._order_id: # 发送通知
|
||||
await self.order_did_success(self._order_id)
|
||||
return True
|
||||
return False
|
||||
finally:
|
||||
pass
|
||||
|
||||
async def order_did_success(self, order_id: str):
|
||||
""" 下单成功 """
|
||||
# 加入小黑屋,防止重复下单
|
||||
await self._add_to_dark_room()
|
||||
title = f'# 车票购买成功,订单号 {order_id} #'
|
||||
content = f"请及时登录12306账号 [{self.user.user_id}],打开 '未完成订单',在30分钟内完成支付!\n"
|
||||
content += f"车次信息: {self.ticket.train_number} {self.query.left_station} -> {self.query.arrive_station},乘车日期 {self.ticket.left_date},席位:{self.ticket.available_seat.get('name')}"
|
||||
Logger.info(title)
|
||||
await Notification.send_message(NotifactionMessage(title, content, extra={
|
||||
'name': self.user.real_name,
|
||||
'left_station': self.ticket.left_station,
|
||||
'arrive_station': self.ticket.arrive_station,
|
||||
'set_type': self.ticket.available_seat.get('name'),
|
||||
'orderno': order_id
|
||||
}))
|
||||
await self.show_no_complete_orders()
|
||||
return True
|
||||
|
||||
@retry
|
||||
async def submit_order_request(self):
|
||||
""" 提交下单请求 """
|
||||
data = {
|
||||
'secretStr': self.ticket.secret_str_unquote, # 解密
|
||||
'train_date': self.ticket.left_date_order, # 出发时间
|
||||
'back_train_date': self.ticket.left_date_order, # 返程时间
|
||||
'tour_flag': 'dc', # 旅途类型
|
||||
'purpose_codes': 'ADULT', # 成人 | 学生
|
||||
'query_from_station_name': StationHelper.cn_by_id(self.query.left_station),
|
||||
'query_to_station_name': StationHelper.cn_by_id(self.query.arrive_station),
|
||||
}
|
||||
response = await self.session.otn_left_ticket_submit_order_request(data)
|
||||
result = response.json()
|
||||
if result.get('data') == 'N':
|
||||
Logger.info('提交订单成功')
|
||||
return True
|
||||
else:
|
||||
if str(result.get('messages', '')).find('未处理') >= 0: # 未处理订单
|
||||
Logger.error(f"提交订单失败,{result.get('messages')}")
|
||||
await self.show_no_complete_orders()
|
||||
return False
|
||||
Logger.error(f"提交订单失败,{result.get('messages', '未知错误')}")
|
||||
return False
|
||||
|
||||
@retry
|
||||
async def request_init_dc_page(self):
|
||||
"""
|
||||
请求下单页面 拿到 token
|
||||
:return:
|
||||
"""
|
||||
response = await self.train_user.session.otn_confirm_passenger_init_dc()
|
||||
html = response.text()
|
||||
token = re.search(r'var globalRepeatSubmitToken = \'(.+?)\'', html)
|
||||
form = re.search(r'var ticketInfoForPassengerForm *= *({.+\})', html)
|
||||
order = re.search(r'var orderRequestDTO *= *({.+\})', html)
|
||||
# 系统忙,请稍后重试
|
||||
if html.find('系统忙,请稍后重试') != -1:
|
||||
raise RetryException('请求初始化订单页面失败')
|
||||
try:
|
||||
self.global_repeat_submit_token = token.groups()[0]
|
||||
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
|
||||
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
|
||||
except Exception:
|
||||
raise RetryException('请求初始化订单页面失败')
|
||||
|
||||
return True
|
||||
|
||||
@retry
|
||||
async def check_order_info(self):
|
||||
""" 检查下单信息 """
|
||||
self.make_passenger_ticket_str()
|
||||
data = { #
|
||||
'cancel_flag': 2,
|
||||
'bed_level_order_num': '000000000000000000000000000000',
|
||||
'passengerTicketStr': self._passenger_ticket_str,
|
||||
'oldPassengerStr': self._old_passenger_str,
|
||||
'tour_flag': 'dc',
|
||||
'randCode': '',
|
||||
'whatsSelect': '1',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.global_repeat_submit_token
|
||||
}
|
||||
response = await self.session.otn_confirm_passenger_check_order_info(data)
|
||||
result = response.json()
|
||||
if result.get('data.submitStatus'): # 成功
|
||||
# ifShowPassCode 需要验证码
|
||||
Logger.info('检查订单成功')
|
||||
if result.get('data.ifShowPassCode') != 'N':
|
||||
self.is_need_auth_code = True # TODO
|
||||
# if ( ticketInfoForPassengerForm.isAsync == ticket_submit_order.request_flag.isAsync & & ticketInfoForPassengerForm.queryLeftTicketRequestDTO.ypInfoDetail != "") { 不需要排队检测 js TODO
|
||||
return True
|
||||
else:
|
||||
if result.get('data.checkSeatNum'):
|
||||
error = '无法提交的订单! ' + result.get('data.errMsg')
|
||||
await self._add_to_dark_room()
|
||||
elif not result.get('data.isNoActive'):
|
||||
error = result.get('data.errMsg', result.get('messages.0'))
|
||||
else:
|
||||
error = '出票失败! ' + result.get('data.errMsg')
|
||||
Logger.error(f'检查订单失败,{error or response.reason}')
|
||||
return False
|
||||
|
||||
@retry
|
||||
async def get_queue_count(self):
|
||||
""" 获取队列人数 """
|
||||
data = { #
|
||||
'train_date': '{} 00:00:00 GMT+0800 (China Standard Time)'.format(
|
||||
self.ticket.left_date.strftime("%a %h %d %Y")),
|
||||
'train_no': self.ticket_info_for_passenger_form['queryLeftTicketRequestDTO']['train_no'],
|
||||
'stationTrainCode': self.ticket_info_for_passenger_form['queryLeftTicketRequestDTO']['station_train_code'],
|
||||
'seatType': self.ticket.available_seat.get('order_id'),
|
||||
'fromStationTelecode': self.ticket_info_for_passenger_form['queryLeftTicketRequestDTO']['from_station'],
|
||||
'toStationTelecode': self.ticket_info_for_passenger_form['queryLeftTicketRequestDTO']['to_station'],
|
||||
'leftTicket': self.ticket_info_for_passenger_form['leftTicketStr'],
|
||||
'purpose_codes': self.ticket_info_for_passenger_form['purpose_codes'],
|
||||
'train_location': self.ticket_info_for_passenger_form['train_location'],
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.global_repeat_submit_token,
|
||||
}
|
||||
response = await self.session.otn_confirm_passenger_get_queue_count(data)
|
||||
result = response.json()
|
||||
if result.get('status', False): # 成功
|
||||
# "data": { "count": "66", "ticket": "0,73", "op_2": "false", "countT": "0", "op_1": "true" }
|
||||
# if result.get('isRelogin') == 'Y': # 重新登录 TODO
|
||||
ticket = result.get('data.ticket').split(',') # 余票列表
|
||||
# 这里可以判断 是真实是 硬座还是无座,避免自动分配到无座
|
||||
ticket_number = ticket[0] # 余票
|
||||
if ticket_number is not '充足' and int(ticket_number) <= 0:
|
||||
if self.ticket.available_seat.get('id') == TrainSeat.NO_SEAT: # 允许无座
|
||||
ticket_number = ticket[1]
|
||||
if not int(ticket_number): # 跳过无座
|
||||
Logger.error('接口返回实际为无票,跳过本次排队')
|
||||
await self._add_to_dark_room()
|
||||
return False
|
||||
|
||||
if result.get('data.op_2') == 'true':
|
||||
Logger.error('排队失败,目前排队人数已经超过余票张数')
|
||||
return False
|
||||
|
||||
current_position = int(result.get('data.countT', 0))
|
||||
Logger.info(f'获取排队信息成功,目前排队人数 {current_position}, 余票还剩余 {ticket_number} 张')
|
||||
return True
|
||||
else:
|
||||
# 加入小黑屋 TODO
|
||||
Logger.error(f"排队失败,错误原因: {result.get('messages', result.get('validateMessages', response.reason))}")
|
||||
return False
|
||||
|
||||
@retry
|
||||
async def confirm_order_queue(self):
|
||||
""" 确认排队 """
|
||||
data = { #
|
||||
'passengerTicketStr': self._passenger_ticket_str,
|
||||
'oldPassengerStr': self._old_passenger_str,
|
||||
'randCode': '',
|
||||
'purpose_codes': self.ticket_info_for_passenger_form['purpose_codes'],
|
||||
'key_check_isChange': self.ticket_info_for_passenger_form['key_check_isChange'],
|
||||
'leftTicketStr': self.ticket_info_for_passenger_form['leftTicketStr'],
|
||||
'train_location': self.ticket_info_for_passenger_form['train_location'],
|
||||
'choose_seats': '',
|
||||
'seatDetailType': '000',
|
||||
'whatsSelect': '1',
|
||||
'roomType': '00',
|
||||
'dwAll': 'N',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.global_repeat_submit_token,
|
||||
}
|
||||
|
||||
if self.is_need_auth_code:
|
||||
# TODO
|
||||
pass
|
||||
|
||||
response = await self.session.otn_confirm_passenger_confirm_single_for_queue(data)
|
||||
result = response.json()
|
||||
|
||||
if 'data' in result:
|
||||
# "data": { "submitStatus": true }
|
||||
if result.get('data.submitStatus'): # 成功
|
||||
Logger.info('# 确认排队成功!#')
|
||||
return True
|
||||
else:
|
||||
Logger.error(f"出票失败,{result.get('data.errMsg', response.reason)}")
|
||||
await self._add_to_dark_room()
|
||||
else:
|
||||
Logger.error(f"提交订单失败,{result.get('messages', response.reason)}")
|
||||
return False
|
||||
|
||||
@retry
|
||||
async def wait_order_queue(self):
|
||||
""" 等待订单排队结果 """
|
||||
wait_count = 0
|
||||
start_at = datetime.datetime.now()
|
||||
while (datetime.datetime.now() - start_at).seconds < self.max_queue_wait:
|
||||
# TODO 取消超时订单,待优化
|
||||
wait_count += 1
|
||||
querys = { #
|
||||
'random': str(random.random())[2:],
|
||||
'tourFlag': 'dc',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.global_repeat_submit_token,
|
||||
}
|
||||
response = await self.session.otn_confirm_passenger_query_order_wait_time(querys)
|
||||
result = response.json()
|
||||
if result.get('status') and 'data' in result:
|
||||
""" "data": { "queryOrderWaitTimeStatus": true, "count": 0, "waitTime": -1,
|
||||
"requestId": 6487958947291482523, "waitCount": 0, "tourFlag": "dc", "orderId": "E222646122"}
|
||||
"""
|
||||
result_data = result['data']
|
||||
order_id = result_data.get('orderId')
|
||||
if order_id: # 成功
|
||||
self._order_id = order_id
|
||||
return True
|
||||
elif 'waitTime' in result_data:
|
||||
# 计算等待时间
|
||||
wait_time = int(result_data.get('waitTime'))
|
||||
if wait_time == -1: # 成功
|
||||
# /otn/confirmPassenger/resultOrderForDcQueue 请求订单状态 目前不需要 # 不应该走到这
|
||||
return True
|
||||
elif wait_time == -100: # 重新获取订单号
|
||||
pass
|
||||
elif wait_time >= 0: # 等待
|
||||
Logger.info(f"排队等待中,排队人数 {result_data.get('waitCount', 0)},预计还需要 {wait_time} 秒")
|
||||
else:
|
||||
if wait_time == -2 or wait_time == -3: # -2 失败 -3 订单已撤销
|
||||
Logger.error(f"排队失败,错误原因, {result_data.get('msg')}")
|
||||
return False
|
||||
else: # 未知原因
|
||||
Logger.error(f"排队失败,错误原因, {result_data.get('msg', wait_time)}")
|
||||
return False
|
||||
|
||||
elif result_data.get('msg'): # 失败 对不起,由于您取消次数过多,今日将不能继续受理您的订票请求。1月8日您可继续使用订票功能。
|
||||
# TODO 需要增加判断 直接结束
|
||||
Logger.error(f"排队失败,错误原因, {result_data.get('msg')}")
|
||||
return False
|
||||
elif result.get('messages') or result.get('validateMessages'):
|
||||
Logger.error(f"排队失败,错误原因, {result.get('messages', result.get('validateMessages'))}")
|
||||
return False
|
||||
else:
|
||||
pass
|
||||
Logger.info(f'第 {wait_count} 次排队,请耐心等待')
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return False
|
||||
|
||||
async def _add_to_dark_room(self):
|
||||
Logger.warning(f'# 已将 {self.ticket.dark_room_text} 关入小黑屋 #')
|
||||
await Cache.add_dark_room(self.ticket.baby)
|
||||
|
||||
@retry
|
||||
async def show_no_complete_orders(self):
|
||||
""" 展示未完成订单 """
|
||||
response = await self.session.otn_query_my_order_no_complete()
|
||||
result = response.json()
|
||||
if result.get('status') is True:
|
||||
for order in result.get('data.orderDBList', []):
|
||||
text = f"\n# 待支付订单号 {order.get('sequence_no')} {order.get('order_date')} #\n"
|
||||
text += f"车次 {''.join(order.get('from_station_name_page', []))} - {''.join(order.get('to_station_name_page', []))} {order.get('train_code_page', '')} {order.get('start_train_date_page', '')} 开\n"
|
||||
for ticket in order.get('tickets'):
|
||||
text += f"\t- {ticket.get('passengerDTO.passenger_name', '')} {ticket.get('passengerDTO.passenger_id_type_name', '')} {ticket.get('seat_type_name', '')} {ticket.get('coach_no', '')}车{ticket.get('seat_name', '')} {ticket.get('ticket_type_name', '')} {ticket.get('str_ticket_price_page', '')}元 {ticket.get('ticket_status_name', '')}\n"
|
||||
Logger.info(text)
|
||||
|
||||
def make_passenger_ticket_str(self):
|
||||
""" 生成提交车次的内容 格式:
|
||||
1(seatType),0,1(车票类型:ticket_type_codes),张三(passenger_name),1(证件类型:passenger_id_type_code),xxxxxx(passenger_id_no),xxxx(mobile_no),N
|
||||
passengerTicketStr:
|
||||
张三(passenger_name),1(证件类型:passenger_id_type_code),xxxxxx(passenger_id_no),1_oldPassengerStr
|
||||
"""
|
||||
passenger_tickets = []
|
||||
old_passengers = []
|
||||
available_passengers = self.query.passengers
|
||||
if len(available_passengers) > self.ticket.member_num_take: # 删除人数
|
||||
available_passengers = available_passengers[0:self.ticket.member_num_take]
|
||||
del_ret = [passenger.get('name') + '(' + passenger.get('type_text') + ')' for passenger in
|
||||
available_passengers]
|
||||
Logger.info(f"# 删减后的乘客列表 {', '.join(del_ret)} #")
|
||||
|
||||
for passenger in available_passengers:
|
||||
tmp_str = f"{self.ticket.available_seat.get('order_id')},0,{passenger['type']},{passenger['name']}," \
|
||||
f"{passenger['id_card_type']},{passenger['id_card']},{passenger['mobile']},N,{passenger['enc_str']}_"
|
||||
passenger_tickets.append(tmp_str)
|
||||
|
||||
if int(passenger['type']) is not UserTypeHelper.CHILD:
|
||||
tmp_old_str = f"{passenger['name']},{passenger['id_card_type']},{passenger['id_card']},{passenger['type']}_"
|
||||
old_passengers.append(tmp_old_str)
|
||||
|
||||
self._passenger_ticket_str = ''.join(passenger_tickets).rstrip('_')
|
||||
self._old_passenger_str = ''.join(old_passengers).rstrip('_') + '__ _ _'
|
||||
325
py12306/app/query.py
Normal file
325
py12306/app/query.py
Normal file
@@ -0,0 +1,325 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from app.app import Event, Cache, Notification
|
||||
from app.app import Logger, Config
|
||||
from app.models import QueryJob, Ticket
|
||||
from lib.exceptions import RetryException
|
||||
from lib.hammer import EventItem
|
||||
from lib.helper import TrainSeat, TaskManager, number_of_time_period, retry
|
||||
from lib.request import TrainSession
|
||||
|
||||
|
||||
class QueryTicketManager(TaskManager):
|
||||
|
||||
async def run(self):
|
||||
Logger.info('正在加载查询任务...')
|
||||
while True:
|
||||
await self.make_tasks()
|
||||
self.clean_fuatures()
|
||||
await asyncio.sleep(self.interval)
|
||||
|
||||
@property
|
||||
async def task_total(self):
|
||||
return await QueryJob.filter_available().count()
|
||||
|
||||
async def make_tasks(self):
|
||||
if await self.is_overflow: # 丢弃多余任务
|
||||
self.tasks.popitem()
|
||||
for query_job in await QueryJob.all():
|
||||
if self.get_task(query_job.id):
|
||||
if not query_job.is_available:
|
||||
self.stop_and_drop(query_job.id)
|
||||
Logger.debug(f'任务 {query_job.name_text} 不可用,已停止该任务')
|
||||
continue
|
||||
if query_job.status == query_job.Status.WaitVerify or not query_job.passengers: # 乘客验证
|
||||
Logger.debug(f'验证任务 {query_job.name_text} 乘客信息...')
|
||||
if not query_job.user_id:
|
||||
query_job.status = query_job.Status.Normal
|
||||
await query_job.save()
|
||||
else:
|
||||
await Event.publish(
|
||||
EventItem(Event.EVENT_VERIFY_QUERY_JOB, {'id': query_job.id, 'user_id': query_job.user_id}))
|
||||
continue
|
||||
if await self.is_full:
|
||||
continue
|
||||
if Config.redis_able and query_job.is_alive:
|
||||
Logger.debug(f'任务 {query_job.name_text} 正在运行中,已跳过')
|
||||
continue
|
||||
await self.handle_task(query_job)
|
||||
|
||||
async def handle_task(self, query: QueryJob):
|
||||
""" 添加查询任务 """
|
||||
if not query.is_queryable:
|
||||
Logger.debug(f'任务 {query.name_text} 未满足查询条件,已跳过')
|
||||
return False
|
||||
ticket = QueryTicket(query)
|
||||
Logger.info(f'# 查询任务 [{query.route_time_text}] 已添加到任务中 #')
|
||||
self.add_task(ticket.run(), query.id, ticket)
|
||||
|
||||
|
||||
class QueryTicket:
|
||||
""" 车票查询 """
|
||||
|
||||
def __init__(self, query: QueryJob):
|
||||
self.api_type: str = ''
|
||||
self.session = TrainSession(use_proxy=True, timeout=5)
|
||||
self.query: QueryJob = query
|
||||
self._last_process_at = query.last_process_at
|
||||
self._last_notifaction_at: Optional[datetime] = None
|
||||
self._is_stop = False
|
||||
self.__flag_num: int = 0 # 连续查询失败次数
|
||||
|
||||
@retry()
|
||||
async def get_query_api_type(self) -> str:
|
||||
""" 动态获取查询的接口, 如 leftTicket/query """
|
||||
if self.api_type:
|
||||
return self.api_type
|
||||
response = await self.session.otn_left_ticket_init()
|
||||
if response.status == 200:
|
||||
res = re.search(r'var CLeftTicketUrl = \'(.*)\';', response.text())
|
||||
try:
|
||||
self.api_type = res.group(1)
|
||||
Logger.info(f'更新查询接口地址: {self.api_type}')
|
||||
except (IndexError, AttributeError):
|
||||
raise RetryException('获取车票查询地址失败')
|
||||
return await self.get_query_api_type()
|
||||
|
||||
async def run(self):
|
||||
"""
|
||||
) 更新查询接口地址
|
||||
) 查询可用的 ticket
|
||||
) """
|
||||
await self.get_query_api_type()
|
||||
while self.is_runable:
|
||||
await self.query.refresh_from_db()
|
||||
# 检测同时运行可能导致任务重复
|
||||
if self.query.last_process_at != self._last_process_at:
|
||||
break
|
||||
self._last_process_at = await self.query.update_last_process_at()
|
||||
fuatures = []
|
||||
try:
|
||||
for _ in range(0, Config.get('proxy.concurrent_num', 1) if Config.proxy_able else 1):
|
||||
fuatures.append(asyncio.ensure_future(self.query_tickets()))
|
||||
await asyncio.wait(fuatures)
|
||||
# await self.query_tickets()
|
||||
except Exception as e:
|
||||
Logger.error(f'查询错误 {e}')
|
||||
finally:
|
||||
await self.query.save()
|
||||
# 下单 TODO
|
||||
# await asyncio.sleep(5)
|
||||
if Config.IS_IN_TEST:
|
||||
break
|
||||
|
||||
async def query_tickets(self):
|
||||
""" 余票查询 """
|
||||
query_num = self.query.query_num_next
|
||||
query = copy.deepcopy(self.query)
|
||||
Logger.info('')
|
||||
Logger.info(f">> 第 {query_num} 次查询 {query.route_text.replace('-', '👉')} {datetime.datetime.now()}")
|
||||
for left_date in query.left_dates:
|
||||
query.left_date = left_date
|
||||
if not query.current_is_queryable:
|
||||
continue
|
||||
for station in query.stations:
|
||||
query.left_station, query.arrive_station = station
|
||||
tickets, stay_interval = await self.get_available_tickets(query)
|
||||
for ticket in tickets:
|
||||
# 验证完成,准备下单
|
||||
Logger.info(
|
||||
f"[ 查询到座位可用 出发时间 {query.left_date} 车次 {ticket.train_number} 座位类型 {ticket.available_seat.get('name')} 余票数量 {ticket.ticket_num} ]")
|
||||
if not Config.IS_IN_TEST:
|
||||
await self._make_order_happen(query, ticket)
|
||||
await asyncio.sleep(stay_interval)
|
||||
|
||||
@retry()
|
||||
async def get_available_tickets(self, query: QueryJob):
|
||||
""" 查询余票 """
|
||||
available_tickets = []
|
||||
output_train_nums = []
|
||||
tickets = await self.get_tickets_from_query(query)
|
||||
for ticket in tickets:
|
||||
if self.verify_train_number(ticket, query):
|
||||
output_train_nums.append(ticket.train_number)
|
||||
if not self.is_ticket_valid(ticket):
|
||||
continue
|
||||
available_tickets.append(ticket)
|
||||
tabs = '\t'
|
||||
stay_interval = self.get_query_interval(len(tickets) > 0)
|
||||
output_train_nums = output_train_nums or ['无可下单车次']
|
||||
Logger.info(
|
||||
f"出发日期 {query.left_date}: {query.left_station} - {query.arrive_station} {tabs} 车次 "
|
||||
f"{tabs.join(output_train_nums)} {tabs} 停留 {stay_interval:.2f}")
|
||||
return available_tickets, stay_interval
|
||||
|
||||
@retry
|
||||
async def get_tickets_from_query(self, query: QueryJob) -> List[Ticket]:
|
||||
response = await self.session.otn_query_left_ticket(await self.get_query_api_type(), query)
|
||||
if response.status is not 200:
|
||||
Logger.error(f'车票查询失败, 状态码 {response.status}, {response.reason} 请求被拒绝')
|
||||
raise RetryException(wait_s=1, default=[])
|
||||
result = response.json().get('data.result')
|
||||
if not result:
|
||||
Logger.error(f'车票查询失败, {response.reason}')
|
||||
return []
|
||||
return Ticket.parse_tickets_text(result)
|
||||
|
||||
def is_ticket_valid(self, ticket: Ticket) -> bool:
|
||||
"""
|
||||
验证 Ticket 信息是否可用
|
||||
) 出发日期验证
|
||||
) 车票数量验证
|
||||
) 时间点验证(00:00 - 24:00)
|
||||
) 车次验证
|
||||
) 座位验证
|
||||
) 乘车人数验证
|
||||
:param ticket: 车票信息
|
||||
:param query: 查询条件
|
||||
:return:
|
||||
"""
|
||||
if not self.verify_ticket_num(ticket):
|
||||
return False
|
||||
|
||||
if not self.verify_period(ticket.left_time, self.query.left_periods):
|
||||
return False
|
||||
|
||||
if not self.verify_train_number(ticket, self.query):
|
||||
return False
|
||||
|
||||
if not self.verify_seat(ticket, self.query):
|
||||
return False
|
||||
if not self.verify_member_count(ticket, self.query):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_period(period: str, available_periods: List[str]):
|
||||
""" 时间点验证(00:00 - 24:00) """
|
||||
if not available_periods:
|
||||
return True
|
||||
period = number_of_time_period(period)
|
||||
if period < number_of_time_period(available_periods[0]) or \
|
||||
period > number_of_time_period(available_periods[1]):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_ticket_num(ticket: Ticket):
|
||||
""" 车票数量验证 """
|
||||
return ticket.ticket_num == 'Y' and ticket.order_text == '预订'
|
||||
|
||||
@classmethod
|
||||
def verify_seat(cls, ticket: Ticket, query: QueryJob) -> bool:
|
||||
""" 检查座位是否可用
|
||||
TODO 小黑屋判断 通过 车次 + 座位
|
||||
"""
|
||||
allow_seats = query.allow_seats
|
||||
for seat in allow_seats:
|
||||
seat_id = TrainSeat.ticket_id[seat]
|
||||
raw = ticket.raw[seat_id]
|
||||
if cls.verify_seat_text(raw):
|
||||
# TODO order model
|
||||
ticket.available_seat = {
|
||||
'name': seat,
|
||||
'id': seat_id,
|
||||
'raw': raw,
|
||||
'order_id': TrainSeat.order_id[seat]
|
||||
}
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def verify_seat_text(seat: str) -> bool:
|
||||
return seat != '' and seat != '无' and seat != '*'
|
||||
|
||||
@staticmethod
|
||||
def verify_member_count(ticket: Ticket, query: QueryJob) -> bool:
|
||||
""" 乘车人数验证 """
|
||||
# TODO 多座位类型判断
|
||||
ticket.member_num_take = query.member_num
|
||||
seat_raw = ticket.available_seat.get('raw', '')
|
||||
if not (seat_raw == '有' or query.member_num <= int(seat_raw)):
|
||||
rest_num = int(seat_raw)
|
||||
if query.less_member:
|
||||
ticket.member_num_take = rest_num
|
||||
Logger.info(f'余票数小于乘车人数,当前余票数: {rest_num}, 实际人数 {query.member_num}, 删减人车人数到: {ticket.member_num_take}')
|
||||
else:
|
||||
Logger.info(f'余票数 {rest_num} 小于乘车人数 {query.member_num},放弃此次提交机会')
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def verify_train_number(ticket: Ticket, query: QueryJob) -> bool:
|
||||
""" 车次验证 """
|
||||
if query.allow_train_numbers and ticket.train_number not in query.allow_train_numbers:
|
||||
return False
|
||||
if query.execpt_train_numbers and ticket.train_number in query.execpt_train_numbers:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_query_interval(self, flag: bool = True):
|
||||
""" 获取查询等待间隔,代理开启时无需等待 """
|
||||
if Config.proxy_able:
|
||||
return 0
|
||||
if flag:
|
||||
self.__flag_num = 0
|
||||
interval = Config.get('query_interval', 1)
|
||||
rand = random.randint(1, 10) * 0.05
|
||||
self.__flag_num += 1
|
||||
return round(interval + rand + self.__flag_num * 0.5, 2)
|
||||
|
||||
async def _make_order_happen(self, query: QueryJob, ticket: Ticket):
|
||||
""" 生成下单事件 """
|
||||
if await Cache.in_dark_room(ticket.baby):
|
||||
Logger.info(f'{ticket.train_number} 已关进小黑屋,跳过本次下单')
|
||||
return
|
||||
if query.user_id:
|
||||
# 这里尽量减少网络传输数据大小,只传递必要数据
|
||||
await Event.publish(EventItem(Event.EVENT_ORDER_TICKET, {
|
||||
'user_id': query.user_id,
|
||||
'query_job': {
|
||||
'hash_id': query.hash_id,
|
||||
'left_date': query.left_date,
|
||||
'left_station': query.left_station,
|
||||
'arrive_station': query.arrive_station,
|
||||
'passengers': query.passengers,
|
||||
},
|
||||
'ticket': {
|
||||
'left_date': ticket.left_date,
|
||||
'hash_id': ticket.hash_id,
|
||||
'train_number': ticket.train_number,
|
||||
'secret_str': ticket.secret_str,
|
||||
'available_seat': ticket.available_seat,
|
||||
'member_num_take': ticket.member_num_take,
|
||||
}
|
||||
}))
|
||||
else:
|
||||
# TODO
|
||||
if self._last_notifaction_at and (datetime.datetime.now() - self._last_notifaction_at).seconds < 60:
|
||||
Logger.info(f'{ticket.train_number} 通知间隔过短,跳过本次通知')
|
||||
else:
|
||||
self._last_notifaction_at = datetime.datetime.now()
|
||||
await Notification.ticket_available_notifation(ticket)
|
||||
Logger.info('余票提醒信息发送成功!')
|
||||
await ticket.save()
|
||||
await query.save()
|
||||
|
||||
def stop(self):
|
||||
if self.is_stoped:
|
||||
return
|
||||
self._is_stop = True
|
||||
Logger.info(f'# 任务 id {self.query.id},{self.query.left_station} - {self.query.arrive_station} 已退出 #')
|
||||
|
||||
@property
|
||||
def is_stoped(self):
|
||||
return self._is_stop
|
||||
|
||||
@property
|
||||
def is_runable(self):
|
||||
return not self._is_stop and self.query.is_queryable
|
||||
421
py12306/app/user.py
Normal file
421
py12306/app/user.py
Normal file
@@ -0,0 +1,421 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from base64 import b64decode
|
||||
from typing import List
|
||||
|
||||
from app.app import Config, Logger, Event, App
|
||||
from app.models import User, QueryJob
|
||||
from lib.exceptions import RetryException, PassengerNotFoundException
|
||||
from lib.helper import ShareInstance, UserTypeHelper, TaskManager, retry
|
||||
from lib.request import TrainSession, Session
|
||||
|
||||
|
||||
class CaptchaTool(ShareInstance):
|
||||
""" 登录验证码类 """
|
||||
|
||||
def __init__(self, session=None):
|
||||
self.session = session or TrainSession.share()
|
||||
|
||||
@retry()
|
||||
async def auth_and_get_answer(self):
|
||||
""" 验证验证码,获取答案 """
|
||||
captcha_image64 = await self.get_base64_code()
|
||||
identify_res = await self.identify_captcha(captcha_image64)
|
||||
if identify_res:
|
||||
ret = await self.verify_captcha_answer(identify_res)
|
||||
if ret:
|
||||
return identify_res
|
||||
# TODO retry
|
||||
return False
|
||||
|
||||
@retry()
|
||||
async def get_base64_code(self) -> str:
|
||||
Logger.debug('正在下载验证码...')
|
||||
response = await self.session.passport_captcha_image64()
|
||||
result = response.json()
|
||||
if result.get('image'):
|
||||
Logger.debug('验证码下载成功')
|
||||
return result.get('image')
|
||||
|
||||
raise RetryException()
|
||||
|
||||
@retry()
|
||||
async def identify_captcha(self, captcha_image64: str):
|
||||
Logger.debug('正在识别验证码...')
|
||||
response = await Session.share().identify_captcha(captcha_image64)
|
||||
result = response.json()
|
||||
if result.get('msg') == 'success':
|
||||
pos = result.get('result')
|
||||
ret = self.get_image_position_by_offset(pos)
|
||||
Logger.debug(f'验证码识别成功,{ret}')
|
||||
return ','.join(map(str, ret))
|
||||
|
||||
Logger.error(f'验证码识别失败,{response.reason}')
|
||||
return False
|
||||
|
||||
@retry()
|
||||
async def verify_captcha_answer(self, answer: str) -> bool:
|
||||
""" 校验验证码 """
|
||||
Logger.debug('正在校验验证码...')
|
||||
response = await self.session.passport_captcha_check(answer)
|
||||
result = response.json()
|
||||
if result.get('result_code') == '4':
|
||||
Logger.info('验证码验证成功')
|
||||
return True
|
||||
else:
|
||||
# {'result_message': '验证码校验失败', 'result_code': '5'}
|
||||
Logger.warning('验证码验证失败 错误原因: %s' % result.get('result_message'))
|
||||
# TODO clean session
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_image_position_by_offset(offsets) -> list:
|
||||
""" 坐标转换到像素 """
|
||||
positions = []
|
||||
width = 75
|
||||
height = 75
|
||||
for offset in offsets:
|
||||
random_x = random.randint(-5, 5)
|
||||
random_y = random.randint(-5, 5)
|
||||
offset = int(offset)
|
||||
x = width * ((offset - 1) % 4 + 1) - width / 2 + random_x
|
||||
y = height * math.ceil(offset / 4) - height / 2 + random_y
|
||||
positions.append(int(x))
|
||||
positions.append(int(y))
|
||||
return positions
|
||||
|
||||
|
||||
class TrainUserManager(TaskManager):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
async def run(self):
|
||||
self.fuatures.append(asyncio.ensure_future(self.subscribe_loop()))
|
||||
while True:
|
||||
await self.make_tasks()
|
||||
self.clean_fuatures()
|
||||
await asyncio.sleep(self.interval)
|
||||
|
||||
@property
|
||||
async def task_total(self) -> int:
|
||||
return await User.filter(enable=True).count()
|
||||
|
||||
@property
|
||||
async def capacity_num(self) -> int:
|
||||
if not App.check_12306_service_time(): # debug 拦截,非服务时间登录不可用
|
||||
Logger.warning('12306 休息时间,已停用登录')
|
||||
return 0
|
||||
return await super().capacity_num
|
||||
|
||||
async def subscribe_loop(self):
|
||||
while True:
|
||||
event = await Event.subscribe()
|
||||
if event.name == Event.EVENT_VERIFY_QUERY_JOB:
|
||||
await self.verify_query_job(event.data)
|
||||
|
||||
async def make_tasks(self):
|
||||
if await self.is_overflow: # 丢弃多余任务
|
||||
self.tasks.popitem()
|
||||
for user in await User.all():
|
||||
if self.get_task(user.id):
|
||||
if not user.enable:
|
||||
self.stop_and_drop(user.id)
|
||||
Logger.debug(f'任务 {user.name_text} 不可用,已停止该任务')
|
||||
continue
|
||||
if await self.is_full:
|
||||
continue
|
||||
if Config.redis_able and user.is_alive:
|
||||
Logger.debug(f'任务 {user.name_text} 正在运行中已跳过')
|
||||
continue
|
||||
await self.handle_task(user)
|
||||
|
||||
async def handle_task(self, user: User):
|
||||
""" 添加任务 """
|
||||
train_user = TrainUser(user)
|
||||
Logger.info(f'# 用户 {user.name} 已添加到任务中 #')
|
||||
self.add_task(train_user.run(), user.id, train_user)
|
||||
|
||||
async def verify_query_job(self, data: dict):
|
||||
""" 验证查询任务信息
|
||||
) 乘客信息验证
|
||||
"""
|
||||
if not isinstance(data, dict) or not data.get('id'):
|
||||
return
|
||||
user_id = data.get('user_id', 0)
|
||||
task: TrainUser = self.get_task(user_id)
|
||||
if not task or not task.is_ready:
|
||||
return
|
||||
query_job = await QueryJob.filter(id=data['id']).first()
|
||||
if not query_job:
|
||||
return
|
||||
ret = task.verify_members(query_job)
|
||||
if ret:
|
||||
query_job.status = query_job.Status.Normal
|
||||
else:
|
||||
query_job.status = query_job.Status.Error
|
||||
query_job.last_error = '乘客验证失败'
|
||||
await query_job.save()
|
||||
return
|
||||
|
||||
|
||||
class TrainUser(ShareInstance):
|
||||
def __init__(self, user: User) -> None:
|
||||
super().__init__()
|
||||
self.session = TrainSession()
|
||||
self.user: User = user
|
||||
self._last_process_at = user.last_process_at
|
||||
self._is_ready = False
|
||||
self._is_stop = False
|
||||
self.is_ordering = False
|
||||
|
||||
async def run(self):
|
||||
await self.login_user()
|
||||
while not self.is_stoped:
|
||||
await self.user.refresh_from_db()
|
||||
# 检测同时运行可能导致任务重复
|
||||
if self.user.last_process_at != self._last_process_at:
|
||||
break
|
||||
self._last_process_at = await self.user.update_last_process_at()
|
||||
await self._heartbeat_check()
|
||||
await asyncio.sleep(Config.USER_HEARTBEAT_INTERVAL)
|
||||
if Config.IS_IN_TEST:
|
||||
break
|
||||
|
||||
@retry
|
||||
async def login_user(self):
|
||||
"""
|
||||
用户登录
|
||||
) 检查用户是否可以恢复
|
||||
) 获取浏览器 ID
|
||||
) 获取验证码识别结果
|
||||
) 请求登录
|
||||
) 获取用户详细信息
|
||||
:param user:
|
||||
:return:
|
||||
"""
|
||||
if await self._try_restore_user():
|
||||
return True
|
||||
data = {
|
||||
'username': self.user.name,
|
||||
'password': self.user.password,
|
||||
'appid': 'otn'
|
||||
}
|
||||
answer = await CaptchaTool(self.session).auth_and_get_answer()
|
||||
data['answer'] = answer
|
||||
await self.update_device_id()
|
||||
response = await self.session.passport_web_login(data)
|
||||
result = response.json()
|
||||
if result.get('result_code') == 0: # 登录成功
|
||||
return await self.handle_login_next_step()
|
||||
elif result.get('result_code') == 2: # 账号之内错误
|
||||
Logger.error(f"登录失败,错误原因: {result.get('result_message')}")
|
||||
else:
|
||||
Logger.error(f"登录失败,{result.get('result_message', '请求被限制')}")
|
||||
raise RetryException(wait_s=5)
|
||||
|
||||
async def handle_login_next_step(self):
|
||||
"""
|
||||
login 获得 cookie uamtk
|
||||
auth/uamtk 不请求,会返回 uamtk票据内容为空
|
||||
/otn/uamauthclient 能拿到用户名
|
||||
"""
|
||||
uamtk = await self.get_auth_uamtk()
|
||||
user_name = await self.get_auth_username(uamtk)
|
||||
await self.login_succeeded()
|
||||
self._welcome_user()
|
||||
return True
|
||||
|
||||
async def update_device_id(self):
|
||||
""" 获取加密后的浏览器特征 ID """
|
||||
response = await Session.share().browser_device_id_url()
|
||||
if response.status == 200:
|
||||
result = response.json()
|
||||
response = await self.session.browser_device_id(b64decode(result['id']).decode())
|
||||
text = response.text()
|
||||
if text.find('callbackFunction') >= 0:
|
||||
result = text[18:-2]
|
||||
result = json.loads(result)
|
||||
self.session.session.cookie_jar.update_cookies({
|
||||
'RAIL_EXPIRATION': result.get('exp'),
|
||||
'RAIL_DEVICEID': result.get('dfp'),
|
||||
})
|
||||
# TODO 错误处理
|
||||
return False
|
||||
|
||||
@retry
|
||||
async def get_auth_uamtk(self) -> str:
|
||||
""" 获取登录 uamtk """
|
||||
response = await self.session.passport_web_auth_uamtk()
|
||||
result = response.json()
|
||||
if result.get('newapptk'):
|
||||
return result.get('newapptk')
|
||||
raise RetryException('获取 uamtk 失败')
|
||||
|
||||
async def get_auth_username(self, uamtk: str):
|
||||
""" 获取登录用户名 """
|
||||
response = await self.session.otn_uamauthclient(uamtk)
|
||||
result = response.json()
|
||||
if result.get('username'):
|
||||
return result.get('username')
|
||||
raise RetryException('获取 username 失败')
|
||||
|
||||
async def login_succeeded(self):
|
||||
""" 登录成功
|
||||
) 更新用户信息
|
||||
"""
|
||||
await self.update_user_info()
|
||||
self._save_user_cookies()
|
||||
await self.user.save()
|
||||
self._is_ready = True
|
||||
|
||||
def _welcome_user(self):
|
||||
Logger.info(f'# 欢迎回来,{self.user.real_name} #')
|
||||
|
||||
def _save_user_cookies(self):
|
||||
self.user.last_cookies = self.session.cookie_dumps()
|
||||
|
||||
async def update_user_info(self):
|
||||
ret: dict = await self.get_user_info()
|
||||
self.user.real_name = ret.get('name', '')
|
||||
self.user.user_id = ret.get('user_name', '')
|
||||
# 更新最后心跳
|
||||
self.user.last_heartbeat = datetime.datetime.now()
|
||||
# 乘客列表
|
||||
self.user.passengers = await self.get_user_passengers()
|
||||
|
||||
@retry
|
||||
async def get_user_info(self) -> dict:
|
||||
""" 获取用户详情 """
|
||||
response = await self.session.otn_modify_user_init_query_user_info()
|
||||
result = response.json()
|
||||
user_info = result.get('data.userDTO.loginUserDTO')
|
||||
if not user_info:
|
||||
raise RetryException('获取用户详情失败,请检测用户是否登录')
|
||||
return user_info
|
||||
|
||||
@retry
|
||||
async def get_user_passengers(self):
|
||||
""" 获取乘客列表 """
|
||||
response = await self.session.otn_confirm_passenger_get_passenger()
|
||||
result = response.json()
|
||||
if result.get('data.normal_passengers'):
|
||||
return result.get('data.normal_passengers')
|
||||
Logger.error(f"获取用户乘客列表失败,{result.get('messages', response.reason)}")
|
||||
raise RetryException
|
||||
|
||||
async def _heartbeat_check(self):
|
||||
""" 心跳检测 """
|
||||
if (datetime.datetime.now() - await self.get_last_heartbeat()).seconds > Config.USER_HEARTBEAT_INTERVAL:
|
||||
return True
|
||||
if not await self.is_still_logged():
|
||||
self._is_ready = False
|
||||
self.user.last_cookies = None
|
||||
await self.user.save()
|
||||
return await self.login_user()
|
||||
|
||||
await self.login_succeeded()
|
||||
Logger.info(f'用户 {self.user.real_name} 心跳正常,下次检测 {Config.USER_HEARTBEAT_INTERVAL} 秒后')
|
||||
|
||||
async def _try_restore_user(self) -> bool:
|
||||
""" 尝试通过 Cookie 恢复用户 """
|
||||
if not self.user.last_cookies:
|
||||
return False
|
||||
self.session.cookie_loads(self.user.last_cookies)
|
||||
if not await self.is_still_logged():
|
||||
# 清空 Cookie
|
||||
self.session.cookie_clean()
|
||||
Logger.info('用户恢复失败,用户状态已过期,正在重新登录...')
|
||||
return False
|
||||
|
||||
await self.login_succeeded()
|
||||
Logger.info(f'# 用户恢复成功,欢迎回来,{self.user.real_name} #')
|
||||
return True
|
||||
|
||||
async def get_last_heartbeat(self) -> datetime.datetime:
|
||||
return self.user.last_heartbeat
|
||||
|
||||
@retry
|
||||
async def is_still_logged(self) -> bool:
|
||||
""" 验证当前登录状态 """
|
||||
response = await self.session.otn_login_conf()
|
||||
result = response.json()
|
||||
if response.status is not 200:
|
||||
raise RetryException
|
||||
if result.get('data.is_login') == 'Y':
|
||||
return True
|
||||
return False
|
||||
|
||||
def format_passengers(self, members: list) -> List[dict]:
|
||||
""" 获取格式化后的乘客列表
|
||||
[{
|
||||
name: '贾宝玉',
|
||||
type: 1,
|
||||
id_card: 0000000000000000000,
|
||||
type_text: '成人',
|
||||
enc_str: 'xxxxxxxx'
|
||||
}]
|
||||
"""
|
||||
ret = []
|
||||
_members_tmp = []
|
||||
for member in members:
|
||||
is_member_id = isinstance(member, int)
|
||||
|
||||
def find_passenger():
|
||||
for item in self.user.passengers:
|
||||
if not is_member_id and member[0] == '*':
|
||||
item['passenger_type'] = UserTypeHelper.ADULT
|
||||
if is_member_id and int(item.get('index_id', -1)) == member or \
|
||||
item.get('passenger_name') == member:
|
||||
if member in _members_tmp:
|
||||
item['passenger_type'] = UserTypeHelper.CHILD
|
||||
return item
|
||||
|
||||
passenger = find_passenger()
|
||||
if not passenger:
|
||||
raise PassengerNotFoundException(member)
|
||||
|
||||
_members_tmp.append(member)
|
||||
ret.append({
|
||||
'name': passenger.get('passenger_name'),
|
||||
'id_card': passenger.get('passenger_id_no'),
|
||||
'id_card_type': passenger.get('passenger_id_type_code'),
|
||||
'mobile': passenger.get('mobile_no'),
|
||||
'type': passenger.get('passenger_type'),
|
||||
'type_text': UserTypeHelper.dicts.get(int(passenger.get('passenger_type'))),
|
||||
'enc_str': passenger.get('allEncStr')
|
||||
})
|
||||
|
||||
return ret
|
||||
|
||||
def verify_members(self, query: QueryJob):
|
||||
try:
|
||||
passengers = self.format_passengers(query.members)
|
||||
if passengers:
|
||||
result = [passenger.get('name') + '(' + passenger.get('type_text') + ')' for passenger in passengers]
|
||||
Logger.info(f"# 乘客验证成功,{query.route_text} {', '.join(result)} #")
|
||||
query.passengers = passengers
|
||||
return True
|
||||
except PassengerNotFoundException as e:
|
||||
Logger.warning(f"# 乘客验证失败,账号 {self.user.name} 中未找到该乘客:{e} #")
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_stoped(self) -> bool:
|
||||
return self._is_stop
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
return self._is_ready
|
||||
|
||||
async def stop(self):
|
||||
if self.is_stoped:
|
||||
return
|
||||
self._is_ready = False
|
||||
self._is_stop = True
|
||||
self._save_user_cookies()
|
||||
await self.user.save()
|
||||
Logger.info(f'# 用户 {self.user.real_name} 已退出 #')
|
||||
@@ -1,274 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
|
||||
import redis
|
||||
from redis.client import PubSub
|
||||
|
||||
from py12306.cluster.redis import Redis
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import *
|
||||
from py12306.log.cluster_log import ClusterLog
|
||||
|
||||
|
||||
@singleton
|
||||
class Cluster():
|
||||
KEY_PREFIX = 'py12306_' # 目前只能手动
|
||||
KEY_QUERY_COUNT = KEY_PREFIX + 'query_count'
|
||||
KEY_QUERY_LAST_TIME = KEY_PREFIX + 'query_last_time'
|
||||
KEY_CONFIGS = KEY_PREFIX + 'configs'
|
||||
KEY_NODES = KEY_PREFIX + 'nodes'
|
||||
KEY_CHANNEL_LOG = KEY_PREFIX + 'channel_log'
|
||||
KEY_CHANNEL_EVENT = KEY_PREFIX + 'channel_even'
|
||||
KEY_USER_COOKIES = KEY_PREFIX + 'user_cookies'
|
||||
KEY_USER_INFOS = KEY_PREFIX + 'user_infos'
|
||||
KEY_USER_LAST_HEARTBEAT = KEY_PREFIX + 'user_last_heartbeat'
|
||||
KEY_NODES_ALIVE_PREFIX = KEY_PREFIX + 'nodes_alive_'
|
||||
|
||||
KEY_CDN_AVAILABLE_ITEMS = KEY_PREFIX + 'cdn_available_items'
|
||||
KEY_CDN_LAST_CHECK_AT = KEY_PREFIX + 'cdn_last_check_at'
|
||||
|
||||
# 锁
|
||||
KEY_LOCK_INIT_USER = KEY_PREFIX + 'lock_init_user' # 暂未使用
|
||||
KEY_LOCK_DO_ORDER = KEY_PREFIX + 'lock_do_order' # 订单锁
|
||||
lock_do_order_time = 60 * 1 # 订单锁超时时间
|
||||
|
||||
lock_prefix = KEY_PREFIX + 'lock_' # 锁键前缀
|
||||
lock_info_prefix = KEY_PREFIX + 'info_'
|
||||
|
||||
KEY_MASTER = 1
|
||||
KEY_SLAVE = 0
|
||||
|
||||
session: Redis = None
|
||||
pubsub: PubSub = None
|
||||
refresh_channel_time = 0.5
|
||||
retry_time = 2
|
||||
keep_alive_time = 3 # 报告存活间隔
|
||||
lost_alive_time = keep_alive_time * 2
|
||||
|
||||
nodes = {}
|
||||
node_name = None
|
||||
is_ready = False
|
||||
is_master = False
|
||||
|
||||
def __init__(self, *args):
|
||||
if Config.is_cluster_enabled():
|
||||
self.session = Redis()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
self.pubsub = self.session.pubsub()
|
||||
self.pubsub.subscribe(self.KEY_CHANNEL_LOG, self.KEY_CHANNEL_EVENT)
|
||||
create_thread_and_run(self, 'subscribe', wait=False)
|
||||
self.is_ready = True
|
||||
self.get_nodes() # 提前获取节点列表
|
||||
self.check_nodes() # 防止 节点列表未清空
|
||||
self.join_cluster()
|
||||
create_thread_and_run(self, 'keep_alive', wait=False)
|
||||
create_thread_and_run(self, 'refresh_data', wait=False)
|
||||
|
||||
def join_cluster(self):
|
||||
"""
|
||||
加入到集群
|
||||
:return:
|
||||
"""
|
||||
self.node_name = node_name = Config().NODE_NAME
|
||||
|
||||
if Config().NODE_IS_MASTER:
|
||||
if self.node_name in self.nodes: # 重复运行主节点
|
||||
ClusterLog.add_quick_log(ClusterLog.MESSAGE_MASTER_NODE_ALREADY_RUN.format(node_name)).flush(
|
||||
publish=False)
|
||||
os._exit(1)
|
||||
if self.have_master(): # 子节点提升为主节点情况,交回控制
|
||||
message = ClusterLog.MESSAGE_NODE_BECOME_MASTER_AGAIN.format(node_name)
|
||||
self.publish_log_message(message)
|
||||
self.make_nodes_as_slave()
|
||||
elif not self.have_master(): # 只能通过主节点启动
|
||||
ClusterLog.add_quick_log(ClusterLog.MESSAGE_MASTER_NODE_NOT_FOUND).flush(publish=False)
|
||||
os._exit(1)
|
||||
|
||||
if node_name in self.nodes:
|
||||
self.node_name = node_name = node_name + '_' + str(dict_count_key_num(self.nodes, node_name))
|
||||
ClusterLog.add_quick_log(ClusterLog.MESSAGE_NODE_ALREADY_IN_CLUSTER.format(node_name)).flush()
|
||||
|
||||
self.session.hset(self.KEY_NODES, node_name, Config().NODE_IS_MASTER)
|
||||
message = ClusterLog.MESSAGE_JOIN_CLUSTER_SUCCESS.format(self.node_name, ClusterLog.get_print_nodes(
|
||||
self.get_nodes())) # 手动 get nodes
|
||||
self.publish_log_message(message)
|
||||
|
||||
def left_cluster(self, node_name=None):
|
||||
node_name = node_name if node_name else self.node_name
|
||||
self.session.hdel(self.KEY_NODES, node_name)
|
||||
message = ClusterLog.MESSAGE_LEFT_CLUSTER.format(node_name, ClusterLog.get_print_nodes(self.get_nodes()))
|
||||
self.publish_log_message(message, node_name)
|
||||
|
||||
def make_nodes_as_slave(self):
|
||||
"""
|
||||
将所有节点设为主节点
|
||||
:return:
|
||||
"""
|
||||
for node in self.nodes:
|
||||
self.session.hset(self.KEY_NODES, node, self.KEY_SLAVE)
|
||||
|
||||
def publish_log_message(self, message, node_name=None):
|
||||
"""
|
||||
发布订阅消息
|
||||
:return:
|
||||
"""
|
||||
node_name = node_name if node_name else self.node_name
|
||||
message = ClusterLog.MESSAGE_SUBSCRIBE_NOTIFICATION.format(node_name, message)
|
||||
self.session.publish(self.KEY_CHANNEL_LOG, message)
|
||||
|
||||
def publish_event(self, name, data={}):
|
||||
"""
|
||||
发布事件消息
|
||||
:return:
|
||||
"""
|
||||
data = {'event': name, 'data': data}
|
||||
self.session.publish(self.KEY_CHANNEL_EVENT, json.dumps(data))
|
||||
|
||||
def get_nodes(self) -> dict:
|
||||
res = self.session.hgetall(self.KEY_NODES)
|
||||
res = res if res else {}
|
||||
self.nodes = res
|
||||
return res
|
||||
|
||||
def refresh_data(self):
|
||||
"""
|
||||
单独进程处理数据同步
|
||||
:return:
|
||||
"""
|
||||
while True:
|
||||
self.get_nodes()
|
||||
self.check_locks()
|
||||
self.check_nodes()
|
||||
self.check_master()
|
||||
stay_second(self.retry_time)
|
||||
|
||||
def check_master(self):
|
||||
"""
|
||||
检测主节点是否可用
|
||||
:return:
|
||||
"""
|
||||
master = self.have_master()
|
||||
if master == self.node_name: # 动态提升
|
||||
self.is_master = True
|
||||
else:
|
||||
self.is_master = False
|
||||
|
||||
if not master:
|
||||
if Config().NODE_SLAVE_CAN_BE_MASTER:
|
||||
# 提升子节点为主节点
|
||||
slave = list(self.nodes)[0]
|
||||
self.session.hset(self.KEY_NODES, slave, self.KEY_MASTER)
|
||||
self.publish_log_message(ClusterLog.MESSAGE_ASCENDING_MASTER_NODE.format(slave,
|
||||
ClusterLog.get_print_nodes(
|
||||
self.get_nodes())))
|
||||
return True
|
||||
else:
|
||||
self.publish_log_message(ClusterLog.MESSAGE_MASTER_DID_LOST.format(self.retry_time))
|
||||
stay_second(self.retry_time)
|
||||
os._exit(1) # 退出整个程序
|
||||
|
||||
def have_master(self):
|
||||
return dict_find_key_by_value(self.nodes, str(self.KEY_MASTER), False)
|
||||
|
||||
def check_nodes(self):
|
||||
"""
|
||||
检查节点是否存活
|
||||
:return:
|
||||
"""
|
||||
for node in self.nodes:
|
||||
if not self.session.exists(self.KEY_NODES_ALIVE_PREFIX + node):
|
||||
self.left_cluster(node)
|
||||
|
||||
# def kick_out_from_nodes(self, node_name):
|
||||
# pass
|
||||
|
||||
def keep_alive(self):
|
||||
while True:
|
||||
if self.node_name not in self.get_nodes(): # 已经被 kict out 重新加下
|
||||
self.join_cluster()
|
||||
self.session.set(self.KEY_NODES_ALIVE_PREFIX + self.node_name, Config().NODE_IS_MASTER, ex=self.lost_alive_time)
|
||||
stay_second(self.keep_alive_time)
|
||||
|
||||
def subscribe(self):
|
||||
while True:
|
||||
try:
|
||||
message = self.pubsub.get_message()
|
||||
except RuntimeError as err:
|
||||
if 'args' in dir(err) and err.args[0].find('pubsub connection not set') >= 0: # 失去重连
|
||||
self.pubsub.subscribe(self.KEY_CHANNEL_LOG, self.KEY_CHANNEL_EVENT)
|
||||
continue
|
||||
if message:
|
||||
if message.get('type') == 'message' and message.get('channel') == self.KEY_CHANNEL_LOG and message.get(
|
||||
'data'):
|
||||
msg = message.get('data')
|
||||
if self.node_name:
|
||||
msg = msg.replace(ClusterLog.MESSAGE_SUBSCRIBE_NOTIFICATION_PREFIX.format(self.node_name), '')
|
||||
ClusterLog.add_quick_log(msg).flush(publish=False)
|
||||
elif message.get('channel') == self.KEY_CHANNEL_EVENT:
|
||||
create_thread_and_run(self, 'handle_events', args=(message,))
|
||||
stay_second(self.refresh_channel_time)
|
||||
|
||||
def handle_events(self, message):
|
||||
# 这里应该分开处理,先都在这处理了
|
||||
if message.get('type') != 'message': return
|
||||
result = json.loads(message.get('data', {}))
|
||||
event_name = result.get('event')
|
||||
data = result.get('data')
|
||||
from py12306.helpers.event import Event
|
||||
method = getattr(Event(), event_name)
|
||||
if method:
|
||||
create_thread_and_run(Event(), event_name, Const.IS_TEST, kwargs={'data': data, 'callback': True})
|
||||
|
||||
def get_lock(self, key: str, timeout=1, info={}):
|
||||
timeout = int(time.time()) + timeout
|
||||
res = self.session.setnx(key, timeout)
|
||||
if res:
|
||||
if info: self.session.set_dict(self.lock_info_prefix + key.replace(self.KEY_PREFIX, ''), info) # 存储额外信息
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_lock_info(self, key, default={}):
|
||||
return self.session.get_dict(self.lock_info_prefix + key.replace(self.KEY_PREFIX, ''), default=default)
|
||||
|
||||
def release_lock(self, key):
|
||||
self.session.delete(key)
|
||||
self.session.delete(self.lock_info_prefix + key.replace(self.KEY_PREFIX, ''))
|
||||
|
||||
def check_locks(self):
|
||||
locks = self.session.keys(self.lock_prefix + '*')
|
||||
for key in locks:
|
||||
val = self.session.get(key)
|
||||
if val and int(val) <= time_int():
|
||||
self.release_lock(key)
|
||||
|
||||
@classmethod
|
||||
def get_user_cookie(cls, key, default=None):
|
||||
self = cls()
|
||||
res = self.session.hget(Cluster.KEY_USER_COOKIES, key)
|
||||
return pickle.loads(res.encode()) if res else default
|
||||
|
||||
@classmethod
|
||||
def set_user_cookie(cls, key, value):
|
||||
self = cls()
|
||||
return self.session.hset(Cluster.KEY_USER_COOKIES, key, pickle.dumps(value, 0).decode())
|
||||
|
||||
@classmethod
|
||||
def set_user_info(cls, key, info):
|
||||
self = cls()
|
||||
return self.session.hset(Cluster.KEY_USER_INFOS, key, pickle.dumps(info, 0).decode())
|
||||
|
||||
@classmethod
|
||||
def get_user_info(cls, key, default=None):
|
||||
self = cls()
|
||||
res = self.session.hget(Cluster.KEY_USER_INFOS, key)
|
||||
return pickle.loads(res.encode()) if res else default
|
||||
@@ -1,59 +0,0 @@
|
||||
import json
|
||||
import pickle
|
||||
|
||||
import redis
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import *
|
||||
from py12306.log.redis_log import RedisLog
|
||||
from redis import Redis as PyRedis
|
||||
|
||||
|
||||
@singleton
|
||||
class Redis(PyRedis):
|
||||
# session = None
|
||||
|
||||
def __init__(self, *args):
|
||||
if Config.is_cluster_enabled():
|
||||
args = {
|
||||
'host': Config().REDIS_HOST,
|
||||
'port': Config().REDIS_PORT,
|
||||
'db': 0,
|
||||
'password': Config().REDIS_PASSWORD,
|
||||
'decode_responses': True
|
||||
}
|
||||
super().__init__(**args)
|
||||
RedisLog.add_quick_log(RedisLog.MESSAGE_REDIS_INIT_SUCCESS)
|
||||
else:
|
||||
super().__init__(**args)
|
||||
return self
|
||||
|
||||
def get(self, name, default=None):
|
||||
res = super().get(name)
|
||||
# if decode: res = res.decode()
|
||||
return res if res else default
|
||||
|
||||
def set(self, name, value, ex=None, px=None, nx=False, xx=False):
|
||||
return super().set(name, available_value(value), ex=ex, px=px, nx=nx, xx=xx)
|
||||
|
||||
def set_dict(self, name, value):
|
||||
return self.set_pickle(name, value)
|
||||
# return self.set(name, json.dumps(value))
|
||||
|
||||
def get_dict(self, name, default={}):
|
||||
return self.get_pickle(name, default)
|
||||
# res = self.get(name)
|
||||
# if res:
|
||||
# return json.loads(res)
|
||||
# return default
|
||||
|
||||
def set_pickle(self, name, value):
|
||||
return self.set(name, pickle.dumps(value, 0).decode())
|
||||
|
||||
def get_pickle(self, name, default=None):
|
||||
res = self.get(name)
|
||||
return pickle.loads(res.encode()) if res else default
|
||||
|
||||
# def smembers(self, name, default=[]):
|
||||
# res = super().smembers(name)
|
||||
# return [val.decode() for val in list(res)] if res else default
|
||||
@@ -1,235 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import re
|
||||
from os import path
|
||||
|
||||
# 12306 账号
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class Config:
|
||||
IS_DEBUG = False
|
||||
|
||||
USER_ACCOUNTS = []
|
||||
# 查询任务
|
||||
QUERY_JOBS = []
|
||||
# 查询间隔
|
||||
QUERY_INTERVAL = 1
|
||||
# 用户心跳检测间隔
|
||||
USER_HEARTBEAT_INTERVAL = 120
|
||||
# 多线程查询
|
||||
QUERY_JOB_THREAD_ENABLED = 0
|
||||
# 打码平台账号
|
||||
AUTO_CODE_PLATFORM = ''
|
||||
AUTO_CODE_ACCOUNT = {'user': '', 'pwd': ''}
|
||||
# 输出日志到文件
|
||||
OUT_PUT_LOG_TO_FILE_ENABLED = 0
|
||||
OUT_PUT_LOG_TO_FILE_PATH = 'runtime/12306.log'
|
||||
|
||||
SEAT_TYPES = {'特等座': 25, '商务座': 32, '一等座': 31, '二等座': 30, '软卧': 23, '硬卧': 28, '硬座': 29, '无座': 26, }
|
||||
|
||||
ORDER_SEAT_TYPES = {'特等座': 'P', '商务座': 9, '一等座': 'M', '二等座': 'O', '软卧': 4, '硬卧': 3, '硬座': 1, '无座': 1}
|
||||
|
||||
PROJECT_DIR = path.dirname(path.dirname(path.abspath(__file__))) + '/'
|
||||
|
||||
# Query
|
||||
RUNTIME_DIR = PROJECT_DIR + 'runtime/'
|
||||
QUERY_DATA_DIR = RUNTIME_DIR + 'query/'
|
||||
USER_DATA_DIR = RUNTIME_DIR + 'user/'
|
||||
USER_PASSENGERS_FILE = RUNTIME_DIR + 'user/%s_passengers.json'
|
||||
|
||||
STATION_FILE = PROJECT_DIR + 'data/stations.txt'
|
||||
CONFIG_FILE = PROJECT_DIR + 'env.py'
|
||||
|
||||
# 语音验证码
|
||||
NOTIFICATION_BY_VOICE_CODE = 0
|
||||
NOTIFICATION_VOICE_CODE_TYPE = ''
|
||||
NOTIFICATION_VOICE_CODE_PHONE = ''
|
||||
NOTIFICATION_API_APP_CODE = ''
|
||||
|
||||
# 集群配置
|
||||
CLUSTER_ENABLED = 0
|
||||
NODE_SLAVE_CAN_BE_MASTER = 1
|
||||
NODE_IS_MASTER = 1
|
||||
NODE_NAME = ''
|
||||
REDIS_HOST = ''
|
||||
REDIS_PORT = '6379'
|
||||
REDIS_PASSWORD = ''
|
||||
|
||||
# 钉钉配置
|
||||
DINGTALK_ENABLED = 0
|
||||
DINGTALK_WEBHOOK = ''
|
||||
|
||||
# Telegram推送配置
|
||||
TELEGRAM_ENABLED = 0
|
||||
TELEGRAM_BOT_API_URL = ''
|
||||
|
||||
# Bark 推送配置
|
||||
BARK_ENABLED = 0
|
||||
BARK_PUSH_URL = ''
|
||||
|
||||
# ServerChan和PushBear配置
|
||||
SERVERCHAN_ENABLED = 0
|
||||
SERVERCHAN_KEY = '8474-ca071ADSFADSF'
|
||||
PUSHBEAR_ENABLED = 0
|
||||
PUSHBEAR_KEY = 'SCUdafadsfasfdafdf45234234234'
|
||||
|
||||
# 邮箱配置
|
||||
EMAIL_ENABLED = 0
|
||||
EMAIL_SENDER = ''
|
||||
EMAIL_RECEIVER = ''
|
||||
EMAIL_SERVER_HOST = ''
|
||||
EMAIL_SERVER_USER = ''
|
||||
EMAIL_SERVER_PASSWORD = ''
|
||||
|
||||
WEB_ENABLE = 0
|
||||
WEB_USER = {}
|
||||
WEB_PORT = 8080
|
||||
WEB_ENTER_HTML_PATH = PROJECT_DIR + 'py12306/web/static/index.html'
|
||||
|
||||
# CDN
|
||||
CDN_ENABLED = 0
|
||||
CDN_CHECK_TIME_OUT = 2
|
||||
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
|
||||
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
|
||||
|
||||
# Default time out
|
||||
TIME_OUT_OF_REQUEST = 5
|
||||
|
||||
envs = []
|
||||
retry_time = 5
|
||||
last_modify_time = 0
|
||||
|
||||
disallow_update_configs = [
|
||||
'CLUSTER_ENABLED',
|
||||
'NODE_IS_MASTER',
|
||||
'NODE_NAME',
|
||||
'REDIS_HOST',
|
||||
'REDIS_PORT',
|
||||
'REDIS_PASSWORD',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.init_envs()
|
||||
self.last_modify_time = get_file_modify_time(self.CONFIG_FILE)
|
||||
if Config().is_slave():
|
||||
self.refresh_configs(True)
|
||||
else:
|
||||
create_thread_and_run(self, 'watch_file_change', False)
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
self.start()
|
||||
|
||||
# @classmethod
|
||||
# def keep_work(cls):
|
||||
# self = cls()
|
||||
|
||||
def start(self):
|
||||
self.save_to_remote()
|
||||
create_thread_and_run(self, 'refresh_configs', wait=Const.IS_TEST)
|
||||
|
||||
def refresh_configs(self, once=False):
|
||||
if not self.is_cluster_enabled(): return
|
||||
while True:
|
||||
remote_configs = self.get_remote_config()
|
||||
self.update_configs_from_remote(remote_configs, once)
|
||||
if once or Const.IS_TEST: return
|
||||
stay_second(self.retry_time)
|
||||
|
||||
def get_remote_config(self):
|
||||
if not self.is_cluster_enabled(): return
|
||||
from py12306.cluster.cluster import Cluster
|
||||
return Cluster().session.get_pickle(Cluster().KEY_CONFIGS, {})
|
||||
|
||||
def save_to_remote(self):
|
||||
if not self.is_master(): return
|
||||
from py12306.cluster.cluster import Cluster
|
||||
Cluster().session.set_pickle(Cluster().KEY_CONFIGS, self.envs)
|
||||
|
||||
def init_envs(self):
|
||||
self.envs = EnvLoader.load_with_file(self.CONFIG_FILE)
|
||||
self.update_configs(self.envs)
|
||||
|
||||
def update_configs(self, envs):
|
||||
for key, value in envs:
|
||||
setattr(self, key, value)
|
||||
|
||||
def watch_file_change(self):
|
||||
"""
|
||||
监听配置文件修改
|
||||
:return:
|
||||
"""
|
||||
if Config().is_slave(): return
|
||||
from py12306.log.common_log import CommonLog
|
||||
while True:
|
||||
value = get_file_modify_time(self.CONFIG_FILE)
|
||||
if value > self.last_modify_time:
|
||||
self.last_modify_time = value
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CONFIG_FILE_DID_CHANGED).flush()
|
||||
envs = EnvLoader.load_with_file(self.CONFIG_FILE)
|
||||
self.update_configs_from_remote(envs)
|
||||
if Config().is_master(): # 保存配置
|
||||
self.save_to_remote()
|
||||
stay_second(self.retry_time)
|
||||
|
||||
def update_configs_from_remote(self, envs, first=False):
|
||||
if envs == self.envs: return
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
from py12306.helpers.cdn import Cdn
|
||||
self.envs = envs
|
||||
for key, value in envs:
|
||||
if key in self.disallow_update_configs: continue
|
||||
if value != -1:
|
||||
old = getattr(self, key)
|
||||
setattr(self, key, value)
|
||||
if not first and old != value:
|
||||
if key == 'USER_ACCOUNTS':
|
||||
User().update_user_accounts(auto=True, old=old)
|
||||
elif key == 'QUERY_JOBS':
|
||||
Query().update_query_jobs(auto=True) # 任务修改
|
||||
elif key == 'QUERY_INTERVAL':
|
||||
Query().update_query_interval(auto=True)
|
||||
elif key == 'CDN_ENABLED':
|
||||
Cdn().update_cdn_status(auto=True)
|
||||
|
||||
@staticmethod
|
||||
def is_master(): # 是不是 主
|
||||
from py12306.cluster.cluster import Cluster
|
||||
return Config().CLUSTER_ENABLED and (Config().NODE_IS_MASTER or Cluster().is_master)
|
||||
|
||||
@staticmethod
|
||||
def is_slave(): # 是不是 从
|
||||
return Config().CLUSTER_ENABLED and not Config.is_master()
|
||||
|
||||
@staticmethod
|
||||
def is_cluster_enabled():
|
||||
return Config().CLUSTER_ENABLED
|
||||
|
||||
@staticmethod
|
||||
def is_cdn_enabled():
|
||||
return Config().CDN_ENABLED
|
||||
|
||||
|
||||
class EnvLoader:
|
||||
envs = []
|
||||
|
||||
def __init__(self):
|
||||
self.envs = []
|
||||
|
||||
@classmethod
|
||||
def load_with_file(cls, file):
|
||||
self = cls()
|
||||
if path.exists(file):
|
||||
env_content = open(file, encoding='utf8').read()
|
||||
content = re.sub(r'^([A-Z]+)_', r'self.\1_', env_content, flags=re.M)
|
||||
exec(content)
|
||||
return self.envs
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
super().__setattr__(key, value)
|
||||
if re.search(r'^[A-Z]+_', key):
|
||||
self.envs.append(([key, value]))
|
||||
@@ -1,73 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import API_FREE_CODE_QCR_API
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.vender.ruokuai.main import RKClient
|
||||
|
||||
|
||||
class OCR:
|
||||
"""
|
||||
图片识别
|
||||
"""
|
||||
session = None
|
||||
|
||||
def __init__(self):
|
||||
self.session = Request()
|
||||
|
||||
@classmethod
|
||||
def get_img_position(cls, img):
|
||||
"""
|
||||
获取图像坐标
|
||||
:param img_path:
|
||||
:return:
|
||||
"""
|
||||
self = cls()
|
||||
if Config().AUTO_CODE_PLATFORM == 'free':
|
||||
return self.get_image_by_free_site(img)
|
||||
return self.get_img_position_by_ruokuai(img)
|
||||
|
||||
def get_img_position_by_ruokuai(self, img):
|
||||
ruokuai_account = Config().AUTO_CODE_ACCOUNT
|
||||
soft_id = '119671'
|
||||
soft_key = '6839cbaca1f942f58d2760baba5ed987'
|
||||
rc = RKClient(ruokuai_account.get('user'), ruokuai_account.get('pwd'), soft_id, soft_key)
|
||||
result = rc.rk_create(img, 6113)
|
||||
if "Result" in result:
|
||||
return self.get_image_position_by_offset(list(result['Result']))
|
||||
CommonLog.print_auto_code_fail(result.get("Error", CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))
|
||||
return None
|
||||
|
||||
def get_image_position_by_offset(self, offsets):
|
||||
positions = []
|
||||
width = 75
|
||||
height = 75
|
||||
for offset in offsets:
|
||||
random_x = random.randint(-5, 5)
|
||||
random_y = random.randint(-5, 5)
|
||||
offset = int(offset)
|
||||
x = width * ((offset - 1) % 4 + 1) - width / 2 + random_x
|
||||
y = height * math.ceil(offset / 4) - height / 2 + random_y
|
||||
positions.append(int(x))
|
||||
positions.append(int(y))
|
||||
return positions
|
||||
|
||||
def get_image_by_free_site(self, img):
|
||||
data = {
|
||||
'img': img
|
||||
}
|
||||
response = self.session.post(API_FREE_CODE_QCR_API, data=data, timeout=30)
|
||||
result = response.json()
|
||||
if result.get('msg') == 'success':
|
||||
pos = result.get('result')
|
||||
return self.get_image_position_by_offset(pos)
|
||||
|
||||
CommonLog.print_auto_code_fail(CommonLog.MESSAGE_GET_RESPONSE_FROM_FREE_AUTO_CODE)
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
# code_result = AuthCode.get_auth_code()
|
||||
@@ -1,50 +0,0 @@
|
||||
# coding=utf-8
|
||||
# 查询余票
|
||||
import time
|
||||
|
||||
HOST_URL_OF_12306 = 'kyfw.12306.cn'
|
||||
BASE_URL_OF_12306 = 'https://' + HOST_URL_OF_12306
|
||||
|
||||
LEFT_TICKETS = {
|
||||
"url": BASE_URL_OF_12306 + "/otn/{type}?leftTicketDTO.train_date={left_date}&leftTicketDTO.from_station={left_station}&leftTicketDTO.to_station={arrive_station}&purpose_codes=ADULT",
|
||||
}
|
||||
|
||||
API_BASE_LOGIN = {
|
||||
"url": BASE_URL_OF_12306 + '/passport/web/login',
|
||||
}
|
||||
|
||||
API_USER_LOGIN_CHECK = BASE_URL_OF_12306 + '/otn/login/conf'
|
||||
|
||||
API_AUTH_CODE_DOWNLOAD = {
|
||||
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&_={random}'
|
||||
}
|
||||
API_AUTH_CODE_BASE64_DOWNLOAD = BASE_URL_OF_12306 + '/passport/captcha/captcha-image64?login_site=E&module=login&rand=sjrand&_={random}'
|
||||
API_AUTH_CODE_CHECK = {
|
||||
'url': BASE_URL_OF_12306 + '/passport/captcha/captcha-check?answer={answer}&rand=sjrand&login_site=E&_={random}'
|
||||
}
|
||||
API_AUTH_UAMTK = {
|
||||
'url': BASE_URL_OF_12306 + '/passport/web/auth/uamtk'
|
||||
}
|
||||
API_AUTH_UAMAUTHCLIENT = {
|
||||
'url': BASE_URL_OF_12306 + '/otn/uamauthclient'
|
||||
}
|
||||
|
||||
API_USER_INFO = {
|
||||
'url': BASE_URL_OF_12306 + '/otn/modifyUser/initQueryUserInfoApi'
|
||||
}
|
||||
API_USER_PASSENGERS = BASE_URL_OF_12306 + '/otn/confirmPassenger/getPassengerDTOs'
|
||||
API_SUBMIT_ORDER_REQUEST = BASE_URL_OF_12306 + '/otn/leftTicket/submitOrderRequest'
|
||||
API_CHECK_ORDER_INFO = BASE_URL_OF_12306 + '/otn/confirmPassenger/checkOrderInfo'
|
||||
API_INITDC_URL = BASE_URL_OF_12306 + '/otn/confirmPassenger/initDc' # 生成订单时需要先请求这个页面
|
||||
API_GET_QUEUE_COUNT = BASE_URL_OF_12306 + '/otn/confirmPassenger/getQueueCount'
|
||||
API_CONFIRM_SINGLE_FOR_QUEUE = BASE_URL_OF_12306 + '/otn/confirmPassenger/confirmSingleForQueue'
|
||||
API_QUERY_ORDER_WAIT_TIME = BASE_URL_OF_12306 + '/otn/confirmPassenger/queryOrderWaitTime?{}' # 排队查询
|
||||
API_QUERY_INIT_PAGE = BASE_URL_OF_12306 + '/otn/leftTicket/init'
|
||||
# API_GET_BROWSER_DEVICE_ID = BASE_URL_OF_12306 + '/otn/HttpZF/logdevice'
|
||||
API_GET_BROWSER_DEVICE_ID = 'https://12306-rail-id-v2.pjialin.com/'
|
||||
API_FREE_CODE_QCR_API = 'https://12306-ocr.pjialin.com/check/'
|
||||
|
||||
API_NOTIFICATION_BY_VOICE_CODE = 'http://ali-voice.showapi.com/sendVoice?'
|
||||
API_NOTIFICATION_BY_VOICE_CODE_DINGXIN = 'http://yuyin2.market.alicloudapi.com/dx/voice_notice'
|
||||
|
||||
API_CHECK_CDN_AVAILABLE = 'https://{}/otn/dynamicJs/omseuuq'
|
||||
@@ -1,84 +0,0 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
from requests.exceptions import SSLError
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.OCR import OCR
|
||||
from py12306.helpers.api import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.helpers.func import *
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.log.user_log import UserLog
|
||||
|
||||
|
||||
class AuthCode:
|
||||
"""
|
||||
验证码类
|
||||
"""
|
||||
session = None
|
||||
data_path = None
|
||||
retry_time = 5
|
||||
|
||||
def __init__(self, session):
|
||||
self.data_path = Config().RUNTIME_DIR
|
||||
self.session = session
|
||||
|
||||
@classmethod
|
||||
def get_auth_code(cls, session):
|
||||
self = cls(session)
|
||||
img = self.download_code()
|
||||
position = OCR.get_img_position(img)
|
||||
if not position: # 打码失败
|
||||
return self.retry_get_auth_code()
|
||||
|
||||
answer = ','.join(map(str, position))
|
||||
|
||||
if not self.check_code(answer):
|
||||
return self.retry_get_auth_code()
|
||||
return position
|
||||
|
||||
def retry_get_auth_code(self): # TODO 安全次数检测
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_RETRY_AUTH_CODE.format(self.retry_time)).flush()
|
||||
time.sleep(self.retry_time)
|
||||
return self.get_auth_code(self.session)
|
||||
|
||||
def download_code(self):
|
||||
url = API_AUTH_CODE_BASE64_DOWNLOAD.format(random=random.random())
|
||||
# code_path = self.data_path + 'code.png'
|
||||
try:
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_DOWNLAODING_THE_CODE).flush()
|
||||
# response = self.session.save_to_file(url, code_path) # TODO 返回错误情况
|
||||
response = self.session.get(url)
|
||||
result = response.json()
|
||||
if result.get('image'):
|
||||
return result.get('image')
|
||||
raise SSLError('返回数据为空')
|
||||
except SSLError as e:
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_DOWNLAOD_AUTH_CODE_FAIL.format(e, self.retry_time)).flush()
|
||||
time.sleep(self.retry_time)
|
||||
return self.download_code()
|
||||
|
||||
def check_code(self, answer):
|
||||
"""
|
||||
校验验证码
|
||||
:return:
|
||||
"""
|
||||
url = API_AUTH_CODE_CHECK.get('url').format(answer=answer, random=time_int())
|
||||
response = self.session.get(url)
|
||||
result = response.json()
|
||||
if result.get('result_code') == '4':
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_CODE_AUTH_SUCCESS).flush()
|
||||
return True
|
||||
else:
|
||||
# {'result_message': '验证码校验失败', 'result_code': '5'}
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_CODE_AUTH_FAIL.format(result.get('result_message'))).flush()
|
||||
self.session.cookies.clear_session_cookies()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
code_result = AuthCode.get_auth_code()
|
||||
@@ -1,234 +0,0 @@
|
||||
import random
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from os import path
|
||||
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.config import Config
|
||||
from py12306.app import app_available_check
|
||||
from py12306.helpers.api import API_CHECK_CDN_AVAILABLE, HOST_URL_OF_12306
|
||||
from py12306.helpers.func import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.log.common_log import CommonLog
|
||||
|
||||
|
||||
@singleton
|
||||
class Cdn:
|
||||
"""
|
||||
CDN 管理
|
||||
"""
|
||||
items = []
|
||||
available_items = []
|
||||
unavailable_items = []
|
||||
recheck_available_items = []
|
||||
recheck_unavailable_items = []
|
||||
retry_time = 3
|
||||
is_ready = False
|
||||
is_finished = False
|
||||
is_ready_num = 10 # 当可用超过 10,已准备好
|
||||
is_alive = True
|
||||
is_recheck = False
|
||||
|
||||
safe_stay_time = 0.2
|
||||
retry_num = 1
|
||||
thread_num = 5
|
||||
check_time_out = 3
|
||||
|
||||
last_check_at = 0
|
||||
save_second = 5
|
||||
check_keep_second = 60 * 60 * 24
|
||||
|
||||
def __init__(self):
|
||||
self.cluster = Cluster()
|
||||
self.init_config()
|
||||
create_thread_and_run(self, 'watch_cdn', False)
|
||||
|
||||
def init_data(self):
|
||||
self.items = []
|
||||
self.available_items = []
|
||||
self.unavailable_items = []
|
||||
self.is_finished = False
|
||||
self.is_ready = False
|
||||
self.is_recheck = False
|
||||
|
||||
def init_config(self):
|
||||
self.check_time_out = Config().CDN_CHECK_TIME_OUT
|
||||
|
||||
def update_cdn_status(self, auto=False):
|
||||
if auto:
|
||||
self.init_config()
|
||||
if Config().is_cdn_enabled():
|
||||
self.run()
|
||||
else:
|
||||
self.destroy()
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
app_available_check()
|
||||
self.is_alive = True
|
||||
self.start()
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
if not Config.is_cdn_enabled(): return
|
||||
self.load_items()
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_START_TO_CHECK.format(len(self.items))).flush()
|
||||
self.restore_items()
|
||||
for i in range(self.thread_num): # 多线程
|
||||
create_thread_and_run(jobs=self, callback_name='check_available', wait=False)
|
||||
|
||||
def load_items(self):
|
||||
with open(Config().CDN_ITEM_FILE, encoding='utf-8') as f:
|
||||
for line, val in enumerate(f):
|
||||
self.items.append(val.rstrip('\n'))
|
||||
|
||||
def restore_items(self):
|
||||
"""
|
||||
恢复已有数据
|
||||
:return: bool
|
||||
"""
|
||||
result = False
|
||||
if path.exists(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE):
|
||||
with open(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE, encoding='utf-8') as f:
|
||||
result = f.read()
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except json.JSONDecodeError as e:
|
||||
result = {}
|
||||
|
||||
# if Config.is_cluster_enabled(): # 集群不用同步 cdn
|
||||
# result = self.get_data_from_cluster()
|
||||
|
||||
if result:
|
||||
self.last_check_at = result.get('last_check_at', '')
|
||||
if self.last_check_at: self.last_check_at = str_to_time(self.last_check_at)
|
||||
self.available_items = result.get('items', [])
|
||||
self.unavailable_items = result.get('fail_items', [])
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_RESTORE_SUCCESS.format(self.last_check_at)).flush()
|
||||
return True
|
||||
return False
|
||||
|
||||
# def get_data_from_cluster(self):
|
||||
# available_items = self.cluster.session.smembers(Cluster.KEY_CDN_AVAILABLE_ITEMS)
|
||||
# last_time = self.cluster.session.get(Cluster.KEY_CDN_LAST_CHECK_AT, '')
|
||||
# if available_items and last_time:
|
||||
# return {'items': available_items, 'last_check_at': last_time}
|
||||
# return False
|
||||
|
||||
def is_need_to_recheck(self):
|
||||
"""
|
||||
是否需要重新检查 cdn
|
||||
:return:
|
||||
"""
|
||||
if self.last_check_at and (
|
||||
time_now() - self.last_check_at).seconds > self.check_keep_second:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_unchecked_item(self):
|
||||
if not self.is_recheck:
|
||||
items = list(set(self.items) - set(self.available_items) - set(self.unavailable_items))
|
||||
else:
|
||||
items = list(set(self.items) - set(self.recheck_available_items) - set(self.recheck_unavailable_items))
|
||||
if items: return random.choice(items)
|
||||
return None
|
||||
|
||||
def check_available(self):
|
||||
while True and self.is_alive:
|
||||
item = self.get_unchecked_item()
|
||||
if not item: return self.check_did_finished()
|
||||
self.check_item_available(item)
|
||||
|
||||
def watch_cdn(self):
|
||||
"""
|
||||
监控 cdn 状态,自动重新检测
|
||||
:return:
|
||||
"""
|
||||
while True:
|
||||
if self.is_alive and not self.is_recheck and self.is_need_to_recheck(): # 重新检测
|
||||
self.is_recheck = True
|
||||
self.is_finished = False
|
||||
CommonLog.add_quick_log(
|
||||
CommonLog.MESSAGE_CDN_START_TO_RECHECK.format(len(self.items), time_now())).flush()
|
||||
for i in range(self.thread_num): # 多线程
|
||||
create_thread_and_run(jobs=self, callback_name='check_available', wait=False)
|
||||
stay_second(self.retry_num)
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
关闭 CDN
|
||||
:return:
|
||||
"""
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_CLOSED).flush()
|
||||
self.is_alive = False
|
||||
self.init_data()
|
||||
|
||||
def check_item_available(self, item, try_num=0):
|
||||
session = Request()
|
||||
response = session.get(API_CHECK_CDN_AVAILABLE.format(item), headers={'Host': HOST_URL_OF_12306},
|
||||
timeout=self.check_time_out,
|
||||
verify=False)
|
||||
|
||||
if response.status_code == 200:
|
||||
if not self.is_recheck:
|
||||
self.available_items.append(item)
|
||||
else:
|
||||
self.recheck_available_items.append(item)
|
||||
if not self.is_ready: self.check_is_ready()
|
||||
elif try_num < self.retry_num: # 重试
|
||||
stay_second(self.safe_stay_time)
|
||||
return self.check_item_available(item, try_num + 1)
|
||||
else:
|
||||
if not self.is_recheck:
|
||||
self.unavailable_items.append(item)
|
||||
else:
|
||||
self.recheck_unavailable_items.append(item)
|
||||
if not self.is_recheck and (
|
||||
not self.last_check_at or (time_now() - self.last_check_at).seconds > self.save_second):
|
||||
self.save_available_items()
|
||||
stay_second(self.safe_stay_time)
|
||||
|
||||
def check_did_finished(self):
|
||||
self.is_ready = True
|
||||
if not self.is_finished:
|
||||
self.is_finished = True
|
||||
if self.is_recheck:
|
||||
self.is_recheck = False
|
||||
self.available_items = self.recheck_available_items
|
||||
self.unavailable_items = self.recheck_unavailable_items
|
||||
self.recheck_available_items = []
|
||||
self.recheck_unavailable_items = []
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_CHECKED_SUCCESS.format(len(self.available_items))).flush()
|
||||
self.save_available_items()
|
||||
|
||||
def save_available_items(self):
|
||||
self.last_check_at = time_now()
|
||||
data = {'items': self.available_items, 'fail_items': self.unavailable_items,
|
||||
'last_check_at': str(self.last_check_at)}
|
||||
with open(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE, 'w') as f:
|
||||
f.write(json.dumps(data))
|
||||
|
||||
# if Config.is_master():
|
||||
# self.cluster.session.sadd(Cluster.KEY_CDN_AVAILABLE_ITEMS, self.available_items)
|
||||
# self.cluster.session.set(Cluster.KEY_CDN_LAST_CHECK_AT, time_now())
|
||||
|
||||
def check_is_ready(self):
|
||||
if len(self.available_items) > self.is_ready_num:
|
||||
self.is_ready = True
|
||||
else:
|
||||
self.is_ready = False
|
||||
|
||||
@classmethod
|
||||
def get_cdn(cls):
|
||||
self = cls()
|
||||
if self.is_ready and self.available_items:
|
||||
return random.choice(self.available_items)
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Const.IS_TEST = True
|
||||
Cdn.run()
|
||||
while not Cdn().is_finished:
|
||||
stay_second(1)
|
||||
@@ -1,48 +0,0 @@
|
||||
from py12306.helpers.func import *
|
||||
from py12306.config import Config
|
||||
|
||||
|
||||
@singleton
|
||||
class Event():
|
||||
"""
|
||||
处理事件
|
||||
"""
|
||||
# 事件
|
||||
KEY_JOB_DESTROY = 'job_destroy'
|
||||
KEY_USER_JOB_DESTROY = 'user_job_destroy'
|
||||
KEY_USER_LOADED = 'user_loaded'
|
||||
cluster = None
|
||||
|
||||
def __init__(self):
|
||||
from py12306.cluster.cluster import Cluster
|
||||
self.cluster = Cluster()
|
||||
|
||||
def job_destroy(self, data={}, callback=False): # 停止查询任务
|
||||
from py12306.query.query import Query
|
||||
if Config().is_cluster_enabled() and not callback:
|
||||
return self.cluster.publish_event(self.KEY_JOB_DESTROY, data) # 通知其它节点退出
|
||||
|
||||
job = Query.job_by_name(data.get('name'))
|
||||
if job:
|
||||
job.destroy()
|
||||
|
||||
def user_loaded(self, data={}, callback=False): # 用户初始化完成
|
||||
if Config().is_cluster_enabled() and not callback:
|
||||
return self.cluster.publish_event(self.KEY_USER_LOADED, data) # 通知其它节点退出
|
||||
from py12306.query.query import Query
|
||||
|
||||
if not Config().is_cluster_enabled() or Config().is_master():
|
||||
query = Query.wait_for_ready()
|
||||
for job in query.jobs:
|
||||
if job.account_key == data.get('key'):
|
||||
create_thread_and_run(job, 'check_passengers', Const.IS_TEST) # 检查乘客信息 防止提交订单时才检查
|
||||
stay_second(1)
|
||||
|
||||
def user_job_destroy(self, data={}, callback=False):
|
||||
from py12306.user.user import User
|
||||
if Config().is_cluster_enabled() and not callback:
|
||||
return self.cluster.publish_event(self.KEY_JOB_DESTROY, data) # 通知其它节点退出
|
||||
|
||||
user = User.get_user(data.get('key'))
|
||||
if user:
|
||||
user.destroy()
|
||||
@@ -1,203 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
import functools
|
||||
import time
|
||||
|
||||
from time import sleep
|
||||
from types import MethodType
|
||||
|
||||
|
||||
def singleton(cls):
|
||||
"""
|
||||
将一个类作为单例
|
||||
来自 https://wiki.python.org/moin/PythonDecoratorLibrary#Singleton
|
||||
"""
|
||||
|
||||
cls.__new_original__ = cls.__new__
|
||||
|
||||
@functools.wraps(cls.__new__)
|
||||
def singleton_new(cls, *args, **kw):
|
||||
it = cls.__dict__.get('__it__')
|
||||
if it is not None:
|
||||
return it
|
||||
|
||||
cls.__it__ = it = cls.__new_original__(cls, *args, **kw)
|
||||
it.__init_original__(*args, **kw)
|
||||
return it
|
||||
|
||||
cls.__new__ = singleton_new
|
||||
cls.__init_original__ = cls.__init__
|
||||
cls.__init__ = object.__init__
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
# 座位 # TODO
|
||||
# def get_number_by_name(name):
|
||||
# return config.SEAT_TYPES[name]
|
||||
|
||||
|
||||
# def get_seat_name_by_number(number): # TODO remove config
|
||||
# return [k for k, v in config.SEAT_TYPES.items() if v == number].pop()
|
||||
|
||||
|
||||
# 初始化间隔
|
||||
def init_interval_by_number(number):
|
||||
if isinstance(number, dict):
|
||||
min = float(number.get('min'))
|
||||
max = float(number.get('max'))
|
||||
else:
|
||||
min = number / 2
|
||||
max = number
|
||||
return {
|
||||
'min': min,
|
||||
'max': max
|
||||
}
|
||||
|
||||
|
||||
def get_interval_num(interval, decimal=2):
|
||||
return round(random.uniform(interval.get('min'), interval.get('max')), decimal)
|
||||
|
||||
|
||||
def stay_second(second, call_back=None):
|
||||
sleep(second)
|
||||
if call_back:
|
||||
return call_back()
|
||||
|
||||
|
||||
def sleep_forever():
|
||||
"""
|
||||
当不是主线程时,假象停止
|
||||
:return:
|
||||
"""
|
||||
if not is_main_thread():
|
||||
while True: sleep(10000000)
|
||||
|
||||
|
||||
def is_main_thread():
|
||||
return threading.current_thread() == threading.main_thread()
|
||||
|
||||
|
||||
def current_thread_id():
|
||||
return threading.current_thread().ident
|
||||
|
||||
|
||||
def time_now():
|
||||
return datetime.datetime.now()
|
||||
|
||||
|
||||
def timestamp_to_time(timestamp):
|
||||
time_struct = time.localtime(timestamp)
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time_struct)
|
||||
|
||||
|
||||
def get_file_modify_time(filePath):
|
||||
timestamp = os.path.getmtime(filePath)
|
||||
return timestamp_to_time(timestamp)
|
||||
|
||||
|
||||
def get_file_total_line_num(file, encoding='utf-8'):
|
||||
with open(file, 'r', encoding=encoding) as f:
|
||||
return len(f.readlines())
|
||||
|
||||
|
||||
def touch_file(path):
|
||||
with open(path, 'a'): pass
|
||||
|
||||
|
||||
def pick_file_lines(file, lines):
|
||||
return [x for i, x in enumerate(file) if i in lines]
|
||||
|
||||
|
||||
def str_to_time(str):
|
||||
return datetime.datetime.strptime(str, '%Y-%m-%d %H:%M:%S.%f')
|
||||
|
||||
|
||||
def time_int():
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def is_number(val):
|
||||
if isinstance(val, int): return val
|
||||
if isinstance(val, str): return val.isdigit()
|
||||
return False
|
||||
|
||||
|
||||
def create_thread_and_run(jobs, callback_name, wait=True, daemon=True, args=(), kwargs={}):
|
||||
threads = []
|
||||
if not isinstance(jobs, list): jobs = [jobs]
|
||||
for job in jobs:
|
||||
thread = threading.Thread(target=getattr(job, callback_name), args=args, kwargs=kwargs)
|
||||
thread.setDaemon(daemon)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
if wait:
|
||||
for thread in threads: thread.join()
|
||||
|
||||
|
||||
def jobs_do(jobs, do):
|
||||
if not isinstance(jobs, list): jobs = [jobs]
|
||||
for job in jobs:
|
||||
getattr(job, do)()
|
||||
|
||||
|
||||
def dict_find_key_by_value(data, value, default=None):
|
||||
result = [k for k, v in data.items() if v == value]
|
||||
return result.pop() if len(result) else default
|
||||
|
||||
|
||||
def objects_find_object_by_key_value(objects, key, value, default=None):
|
||||
result = [obj for obj in objects if getattr(obj, key) == value]
|
||||
return result.pop() if len(result) else default
|
||||
|
||||
|
||||
def dict_count_key_num(data: dict, key, like=False):
|
||||
count = 0
|
||||
for k in data.keys():
|
||||
if like:
|
||||
if k.find(key) >= 0: count += 1
|
||||
elif k == key:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def array_dict_find_by_key_value(data, key, value, default=None):
|
||||
result = [v for k, v in enumerate(data) if key in v and v[key] == value]
|
||||
return result.pop() if len(result) else default
|
||||
|
||||
|
||||
def get_true_false_text(value, true='', false=''):
|
||||
if value: return true
|
||||
return false
|
||||
|
||||
|
||||
def sleep_forever_when_in_test():
|
||||
if Const.IS_TEST: sleep_forever()
|
||||
|
||||
|
||||
def expand_class(cls, key, value, keep_old=True):
|
||||
if (keep_old):
|
||||
setattr(cls, 'old_' + key, getattr(cls, key))
|
||||
setattr(cls, key, MethodType(value, cls))
|
||||
return cls
|
||||
|
||||
|
||||
def available_value(value):
|
||||
if isinstance(value, str) or isinstance(value, bytes):
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
|
||||
def md5(value):
|
||||
return hashlib.md5(json.dumps(value).encode()).hexdigest()
|
||||
|
||||
|
||||
@singleton
|
||||
class Const:
|
||||
IS_TEST = False
|
||||
IS_TEST_NOTIFICATION = False
|
||||
@@ -1,206 +0,0 @@
|
||||
import urllib
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.log.common_log import CommonLog
|
||||
|
||||
|
||||
class Notification():
|
||||
"""
|
||||
通知类
|
||||
"""
|
||||
session = None
|
||||
|
||||
def __init__(self):
|
||||
self.session = Request()
|
||||
|
||||
@classmethod
|
||||
def voice_code(cls, phone, name='', content=''):
|
||||
self = cls()
|
||||
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
|
||||
self.send_voice_code_of_dingxin(phone, name=name, info=content)
|
||||
else:
|
||||
self.send_voice_code_of_yiyuan(phone, name=name, content=content)
|
||||
|
||||
@classmethod
|
||||
def dingtalk_webhook(cls, content=''):
|
||||
self = cls()
|
||||
self.send_dingtalk_by_webbook(content=content)
|
||||
|
||||
@classmethod
|
||||
def send_email(cls, to, title='', content=''):
|
||||
self = cls()
|
||||
self.send_email_by_smtp(to, title, content)
|
||||
|
||||
@classmethod
|
||||
def send_to_telegram(cls, content=''):
|
||||
self = cls()
|
||||
self.send_to_telegram_bot(content=content)
|
||||
|
||||
@classmethod
|
||||
def server_chan(cls, skey='', title='', content=''):
|
||||
self = cls()
|
||||
self.send_serverchan(skey=skey, title=title, content=content)
|
||||
|
||||
@classmethod
|
||||
def push_bear(cls, skey='', title='', content=''):
|
||||
self = cls()
|
||||
self.send_pushbear(skey=skey, title=title, content=content)
|
||||
|
||||
@classmethod
|
||||
def push_bark(cls, content=''):
|
||||
self = cls()
|
||||
self.push_to_bark(content)
|
||||
|
||||
def send_voice_code_of_yiyuan(self, phone, name='', content=''):
|
||||
"""
|
||||
发送语音验证码
|
||||
购买地址 https://market.aliyun.com/products/57126001/cmapi019902.html?spm=5176.2020520132.101.5.37857218O6iJ3n
|
||||
:return:
|
||||
"""
|
||||
appcode = Config().NOTIFICATION_API_APP_CODE
|
||||
if not appcode:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_EMPTY_APP_CODE).flush()
|
||||
return False
|
||||
body = {
|
||||
'userName': name,
|
||||
'mailNo': content
|
||||
}
|
||||
params = {
|
||||
'content': body,
|
||||
'mobile': phone,
|
||||
'sex': 2,
|
||||
'tNum': 'T170701001056'
|
||||
}
|
||||
response = self.session.request(url=API_NOTIFICATION_BY_VOICE_CODE + urllib.parse.urlencode(params),
|
||||
method='GET', headers={'Authorization': 'APPCODE {}'.format(appcode)})
|
||||
result = response.json()
|
||||
response_message = result.get('showapi_res_body.remark')
|
||||
if response.status_code in [400, 401, 403]:
|
||||
return CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_FORBID).flush()
|
||||
if response.status_code == 200 and result.get('showapi_res_body.flag'):
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_SEND_SUCCESS.format(response_message)).flush()
|
||||
return True
|
||||
else:
|
||||
return CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_SEND_FAIL.format(response_message)).flush()
|
||||
|
||||
def send_voice_code_of_dingxin(self, phone, name='', info={}):
|
||||
"""
|
||||
发送语音验证码 ( 鼎信 )
|
||||
购买地址 https://market.aliyun.com/products/56928004/cmapi026600.html?spm=5176.2020520132.101.2.51547218rkAXxy
|
||||
:return:
|
||||
"""
|
||||
appcode = Config().NOTIFICATION_API_APP_CODE
|
||||
if not appcode:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_EMPTY_APP_CODE).flush()
|
||||
return False
|
||||
data = {
|
||||
'tpl_id': 'TP1901174',
|
||||
'phone': phone,
|
||||
'param': 'name:{name},job_name:{left_station}到{arrive_station}{set_type},orderno:{orderno}'.format(
|
||||
name=name, left_station=info.get('left_station'), arrive_station=info.get('arrive_station'),
|
||||
set_type=info.get('set_type'), orderno=info.get('orderno'))
|
||||
}
|
||||
response = self.session.request(url=API_NOTIFICATION_BY_VOICE_CODE_DINGXIN, method='POST', data=data,
|
||||
headers={'Authorization': 'APPCODE {}'.format(appcode)})
|
||||
result = response.json()
|
||||
response_message = result.get('return_code')
|
||||
if response.status_code in [400, 401, 403]:
|
||||
return CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_FORBID).flush()
|
||||
if response.status_code == 200 and result.get('return_code') == '00000':
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_SEND_SUCCESS.format(response_message)).flush()
|
||||
return True
|
||||
else:
|
||||
return CommonLog.add_quick_log(CommonLog.MESSAGE_VOICE_API_SEND_FAIL.format(response_message)).flush()
|
||||
|
||||
def send_email_by_smtp(self, to, title, content):
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
to = to if isinstance(to, list) else [to]
|
||||
message = EmailMessage()
|
||||
message['Subject'] = title
|
||||
message['From'] = Config().EMAIL_SENDER
|
||||
message['To'] = to
|
||||
message.set_content(content)
|
||||
try:
|
||||
server = smtplib.SMTP(Config().EMAIL_SERVER_HOST)
|
||||
server.ehlo()
|
||||
server.starttls()
|
||||
server.login(Config().EMAIL_SERVER_USER, Config().EMAIL_SERVER_PASSWORD)
|
||||
server.send_message(message)
|
||||
server.quit()
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_SUCCESS).flush()
|
||||
except Exception as e:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_EMAIL_FAIL.format(e)).flush()
|
||||
|
||||
def send_dingtalk_by_webbook(self, content):
|
||||
from dingtalkchatbot.chatbot import DingtalkChatbot
|
||||
webhook = Config().DINGTALK_WEBHOOK
|
||||
dingtalk = DingtalkChatbot(webhook)
|
||||
dingtalk.send_text(msg=content, is_at_all=True)
|
||||
pass
|
||||
|
||||
def send_to_telegram_bot(self, content):
|
||||
bot_api_url = Config().TELEGRAM_BOT_API_URL
|
||||
if not bot_api_url:
|
||||
return False
|
||||
data = {
|
||||
'text': content
|
||||
}
|
||||
response = self.session.request(url=bot_api_url, method='POST', data=data)
|
||||
result = response.json().get('result')
|
||||
response_status = result.get('statusCode')
|
||||
if response_status == 200:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_TELEGRAM_SUCCESS).flush()
|
||||
else:
|
||||
response_error_message = result.get('description')
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_TELEGRAM_FAIL.format(response_error_message)).flush()
|
||||
|
||||
def push_to_bark(self, content):
|
||||
bark_url = Config().BARK_PUSH_URL
|
||||
if not bark_url:
|
||||
return False
|
||||
|
||||
response = self.session.request(url=bark_url + '/' + content, method='get')
|
||||
result = response.json()
|
||||
response_status = result.get('code')
|
||||
if response_status == 200:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_BARK_SUCCESS).flush()
|
||||
else:
|
||||
response_error_message = result.get('message')
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_BARK_FAIL.format(response_error_message)).flush()
|
||||
|
||||
def send_serverchan(self, skey, title, content):
|
||||
from lightpush import lightpush
|
||||
lgp = lightpush()
|
||||
lgp.set_single_push(key=skey)
|
||||
try:
|
||||
lgp.single_push(title, content)
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_SERVER_CHAN_SUCCESS).flush()
|
||||
except Exception as e:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_SERVER_CHAN_FAIL.format(e)).flush()
|
||||
|
||||
def send_pushbear(self, skey, title, content):
|
||||
from lightpush import lightpush
|
||||
lgp = lightpush()
|
||||
lgp.set_group_push(key=skey)
|
||||
try:
|
||||
lgp.group_push(title, content)
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_PUSH_BEAR_SUCCESS).flush()
|
||||
except Exception as e:
|
||||
CommonLog.add_quick_log(CommonLog.MESSAGE_SEND_PUSH_BEAR_SUCCESS.format(e)).flush()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
name = '张三4'
|
||||
content = '你的车票 广州 到 深圳 购买成功,请登录 12306 进行支付'
|
||||
# Notification.voice_code('13800138000', name, content)
|
||||
# Notification.send_email('user@email.com', name, content)
|
||||
# Notification.dingtalk_webhook(content)
|
||||
Notification.voice_code('13800138000', name, {
|
||||
'left_station': '广州',
|
||||
'arrive_station': '深圳',
|
||||
'set_type': '硬座',
|
||||
'orderno': 'E123542'
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
import requests
|
||||
from requests.exceptions import *
|
||||
|
||||
from py12306.helpers.func import *
|
||||
from requests_html import HTMLSession, HTMLResponse
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
class Request(HTMLSession):
|
||||
"""
|
||||
请求处理类
|
||||
"""
|
||||
|
||||
# session = {}
|
||||
def save_to_file(self, url, path):
|
||||
response = self.get(url, stream=True)
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
f.write(chunk)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _handle_response(response, **kwargs) -> HTMLResponse:
|
||||
"""
|
||||
扩充 response
|
||||
:param response:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
response = HTMLSession._handle_response(response, **kwargs)
|
||||
expand_class(response, 'json', Request.json)
|
||||
return response
|
||||
|
||||
def add_response_hook(self, hook):
|
||||
hooks = self.hooks['response']
|
||||
if not isinstance(hooks, list):
|
||||
hooks = [hooks]
|
||||
hooks.append(hook)
|
||||
self.hooks['response'] = hooks
|
||||
return self
|
||||
|
||||
def json(self, default={}):
|
||||
"""
|
||||
重写 json 方法,拦截错误
|
||||
:return:
|
||||
"""
|
||||
from py12306.app import Dict
|
||||
try:
|
||||
result = self.old_json()
|
||||
return Dict(result)
|
||||
except:
|
||||
return Dict(default)
|
||||
|
||||
def request(self, *args, **kwargs): # 拦截所有错误
|
||||
try:
|
||||
if not 'timeout' in kwargs:
|
||||
from py12306.config import Config
|
||||
kwargs['timeout'] = Config().TIME_OUT_OF_REQUEST
|
||||
response = super().request(*args, **kwargs)
|
||||
return response
|
||||
except RequestException as e:
|
||||
from py12306.log.common_log import CommonLog
|
||||
if e.response:
|
||||
response = e.response
|
||||
else:
|
||||
response = HTMLResponse(HTMLSession)
|
||||
# response.status_code = 500
|
||||
expand_class(response, 'json', Request.json)
|
||||
response.reason = response.reason if response.reason else CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR
|
||||
return response
|
||||
|
||||
def cdn_request(self, url: str, cdn=None, method='GET', **kwargs):
|
||||
from py12306.helpers.api import HOST_URL_OF_12306
|
||||
from py12306.helpers.cdn import Cdn
|
||||
if not cdn: cdn = Cdn.get_cdn()
|
||||
url = url.replace(HOST_URL_OF_12306, cdn)
|
||||
|
||||
return self.request(method, url, headers={'Host': HOST_URL_OF_12306}, verify=False, **kwargs)
|
||||
@@ -1,46 +0,0 @@
|
||||
from os import path
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class Station:
|
||||
stations = []
|
||||
station_kvs = {}
|
||||
|
||||
def __init__(self):
|
||||
if path.exists(Config().STATION_FILE):
|
||||
result = open(Config().STATION_FILE, encoding='utf-8').read()
|
||||
result = result.lstrip('@').split('@')
|
||||
for i in result:
|
||||
tmp_info = i.split('|')
|
||||
self.stations.append({
|
||||
'key': tmp_info[2],
|
||||
'name': tmp_info[1],
|
||||
'pinyin': tmp_info[3],
|
||||
'id': tmp_info[5]
|
||||
})
|
||||
self.station_kvs[tmp_info[1]] = tmp_info[2]
|
||||
|
||||
@classmethod
|
||||
def get_station_by_name(cls, name):
|
||||
return cls.get_station_by(name, 'name')
|
||||
|
||||
@classmethod
|
||||
def get_station_by(cls, value, field):
|
||||
self = cls()
|
||||
for station in self.stations:
|
||||
if station.get(field) == value:
|
||||
return station
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_station_key_by_name(cls, name):
|
||||
self = cls()
|
||||
return self.station_kvs[name]
|
||||
|
||||
@classmethod
|
||||
def get_station_name_by_key(cls, key):
|
||||
return cls.get_station_by(key, 'key').get('name')
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class UserType:
|
||||
ADULT = 1
|
||||
CHILD = 2
|
||||
STUDENT = 3
|
||||
SOLDIER = 4
|
||||
|
||||
dicts = {
|
||||
'成人': ADULT,
|
||||
'儿童': CHILD,
|
||||
'学生': STUDENT,
|
||||
'残疾军人、伤残人民警察': SOLDIER,
|
||||
}
|
||||
|
||||
|
||||
@singleton
|
||||
class OrderSeatType:
|
||||
dicts = {
|
||||
'特等座': 'P',
|
||||
'商务座': 9,
|
||||
'一等座': 'M',
|
||||
'二等座': 'O',
|
||||
'软卧': 4,
|
||||
'硬卧': 3,
|
||||
'动卧': 1,
|
||||
'软座': 2,
|
||||
'硬座': 1,
|
||||
'无座': 1,
|
||||
}
|
||||
|
||||
|
||||
@singleton
|
||||
class SeatType:
|
||||
NO_SEAT = 26
|
||||
dicts = {
|
||||
'特等座': 25,
|
||||
'商务座': 32,
|
||||
'一等座': 31,
|
||||
'二等座': 30,
|
||||
'软卧': 23,
|
||||
'硬卧': 28,
|
||||
'动卧': 33,
|
||||
'软座': 24,
|
||||
'硬座': 29,
|
||||
'无座': NO_SEAT,
|
||||
}
|
||||
30
py12306/lib/exceptions.py
Normal file
30
py12306/lib/exceptions.py
Normal file
@@ -0,0 +1,30 @@
|
||||
class RetryException(Exception):
|
||||
|
||||
def __init__(self, msg='', default=None, wait_s: int = 0, *args: object) -> None:
|
||||
self.msg = msg
|
||||
self.default = default
|
||||
self.wait_s = wait_s
|
||||
super().__init__(msg, *args)
|
||||
|
||||
|
||||
class MaxRetryException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NeedToImplementException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PassengerNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InstanceAlreadyInitedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LoadConfigFailException(Exception):
|
||||
|
||||
def __init__(self, msg: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.msg = msg
|
||||
262
py12306/lib/hammer.py
Normal file
262
py12306/lib/hammer.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
from abc import abstractmethod, ABC
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
from aioredis import Channel
|
||||
|
||||
from lib.helper import json_friendly_loads, json_friendly_dumps
|
||||
|
||||
|
||||
class EventItem:
|
||||
|
||||
def __init__(self, name: str, data: Any) -> None:
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.data = data
|
||||
|
||||
def dumps(self) -> str:
|
||||
return json_friendly_dumps({'name': self.name, 'data': self.data})
|
||||
|
||||
|
||||
class EventAbstract(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def publish(self, item: EventItem):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def subscribe(self) -> EventItem:
|
||||
pass
|
||||
|
||||
|
||||
class RedisEventEngine(EventAbstract):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
from app.app import Redis, Config
|
||||
self.redis = Redis
|
||||
self._channel_name = Config.APP_NAME
|
||||
self._msg_queue = asyncio.Queue()
|
||||
self._subscribed = False
|
||||
|
||||
async def publish(self, item: EventItem):
|
||||
await self.redis.publish(self._channel_name, item.dumps())
|
||||
|
||||
async def subscribe(self) -> EventItem:
|
||||
if not self._subscribed:
|
||||
self._subscribed = True
|
||||
ret = await self.redis.subscribe(self._channel_name)
|
||||
asyncio.ensure_future(self._handle_msg(ret[0]))
|
||||
return await self._msg_queue.get()
|
||||
|
||||
async def _handle_msg(self, channel: Channel):
|
||||
while await channel.wait_message():
|
||||
msg = await channel.get()
|
||||
await self._msg_queue.put(EventItem(**json_friendly_loads(msg)))
|
||||
|
||||
|
||||
class AsyncioEventEngine(EventAbstract):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._queue_lists = []
|
||||
|
||||
async def publish(self, item: EventItem):
|
||||
for queue in self._queue_lists:
|
||||
await queue.put(item)
|
||||
|
||||
async def subscribe(self) -> EventItem:
|
||||
queue = asyncio.Queue()
|
||||
self._queue_lists.append(queue)
|
||||
return await queue.get()
|
||||
|
||||
|
||||
class EventHammer:
|
||||
EVENT_ORDER_TICKET = 'order_ticket'
|
||||
EVENT_VERIFY_QUERY_JOB = 'verify_query_job'
|
||||
|
||||
def __init__(self, engine: EventAbstract) -> None:
|
||||
super().__init__()
|
||||
self.engine: Optional[EventAbstract] = engine
|
||||
|
||||
async def publish(self, item: EventItem):
|
||||
await self.engine.publish(item)
|
||||
|
||||
async def subscribe(self) -> EventItem:
|
||||
return await self.engine.subscribe()
|
||||
|
||||
|
||||
# // 缓存
|
||||
class CacheAbstract(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def set(self, key: str, val: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get(self, key: str, default: Any = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def lpush(self, key: str, val):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def lget(self, key: str, default: Any = None) -> list:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def sadd(self, key: str, val):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def sget(self, key: str, default: Any = None) -> set:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def hset(self, key: str, field: str, val: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def hget(self, key: str, field: str, default: Any = None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def hdel(self, key: str, field: str):
|
||||
pass
|
||||
|
||||
|
||||
class RedisCacheEngine(CacheAbstract):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
from app.app import Redis
|
||||
self.redis = Redis
|
||||
|
||||
async def set(self, key: str, val: str):
|
||||
return await self.redis.set(key, val)
|
||||
|
||||
async def get(self, key: str, default: Any = None):
|
||||
ret = await self.redis.get(key)
|
||||
if not ret:
|
||||
return default
|
||||
return ret.decode()
|
||||
|
||||
async def lpush(self, key: str, val):
|
||||
pass
|
||||
|
||||
async def lget(self, key: str, default: Any = None) -> list:
|
||||
pass
|
||||
|
||||
async def sadd(self, key: str, val):
|
||||
pass
|
||||
|
||||
async def sget(self, key: str, default: Any = None) -> set:
|
||||
pass
|
||||
|
||||
async def hset(self, key: str, field: str, val: str):
|
||||
return await self.redis.hset(key, field, val)
|
||||
|
||||
async def hget(self, key: str, field: str, default: Any = None):
|
||||
ret = await self.redis.hget(key, field) or default
|
||||
if not ret:
|
||||
return default
|
||||
return ret.decode()
|
||||
|
||||
async def hdel(self, key: str, field: str):
|
||||
return await self.redis.hdel(key, field)
|
||||
|
||||
|
||||
class AsyncioCacheEngine(CacheAbstract):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._queue = asyncio.Queue()
|
||||
self.string_dict = {}
|
||||
self.list_items: Dict[str, list] = {}
|
||||
self.set_items: Dict[str, set] = {}
|
||||
self.hash_items: Dict[str, dict] = {}
|
||||
|
||||
async def set(self, key: str, val: str):
|
||||
self.string_dict[key] = val
|
||||
|
||||
async def get(self, key: str, default: Any = None):
|
||||
self.string_dict.get(key, default)
|
||||
|
||||
async def lpush(self, key: str, val):
|
||||
if key not in self.list_items:
|
||||
self.list_items[key] = []
|
||||
self.list_items[key].append(key)
|
||||
|
||||
async def lget(self, key: str, default: Any = None) -> list:
|
||||
if key not in self.list_items:
|
||||
return default
|
||||
return self.list_items[key]
|
||||
|
||||
async def sadd(self, key: str, val):
|
||||
if key not in self.set_items:
|
||||
self.set_items[key] = set()
|
||||
self.set_items[key].add(key)
|
||||
|
||||
async def sget(self, key: str, default: Any = None) -> set:
|
||||
if key not in self.set_items:
|
||||
return default
|
||||
return self.set_items[key]
|
||||
|
||||
async def hset(self, key: str, field: str, val: str):
|
||||
if key not in self.hash_items:
|
||||
self.hash_items[key] = {}
|
||||
self.hash_items[key][field] = val
|
||||
|
||||
async def hget(self, key: str, field: str, default: Any = None):
|
||||
if key not in self.hash_items:
|
||||
return default
|
||||
return self.hash_items[key].get(field, default)
|
||||
|
||||
async def hdel(self, key: str, field: str):
|
||||
if key not in self.hash_items:
|
||||
return
|
||||
if field in self.hash_items[key]:
|
||||
del self.hash_items[key][field]
|
||||
|
||||
|
||||
class CacheHammer:
|
||||
KEY_DARK_ROOM = 'dark_room'
|
||||
|
||||
def __init__(self, engine: CacheAbstract) -> None:
|
||||
super().__init__()
|
||||
self.engine: Optional[CacheAbstract] = engine
|
||||
|
||||
async def set(self, key: str, val: str):
|
||||
return await self.engine.set(key, val)
|
||||
|
||||
async def get(self, key: str, default: Any = None):
|
||||
return await self.engine.get(key, default)
|
||||
|
||||
async def hset(self, key: str, field: str, val: str):
|
||||
return await self.engine.hset(key, field, val)
|
||||
|
||||
async def hget(self, key: str, field: str, default: Any = None):
|
||||
return await self.engine.hget(key, field, default)
|
||||
|
||||
async def hdel(self, key: str, field: str):
|
||||
return await self.engine.hdel(key, field)
|
||||
|
||||
async def add_dark_room(self, baby: str):
|
||||
return await self.hset(self.KEY_DARK_ROOM, baby, json_friendly_dumps({
|
||||
'id': baby,
|
||||
'created_at': datetime.datetime.now()
|
||||
}))
|
||||
|
||||
async def in_dark_room(self, baby: str) -> bool:
|
||||
_baby = await self.hget(self.KEY_DARK_ROOM, baby)
|
||||
if not _baby:
|
||||
return False
|
||||
_baby = json_friendly_loads(_baby)
|
||||
# 超过限制时长
|
||||
if not isinstance(_baby.get('created_at'), datetime.datetime) or \
|
||||
(datetime.datetime.now() - _baby['created_at']).seconds > 60:
|
||||
await self.hdel(self.KEY_DARK_ROOM, baby)
|
||||
return False
|
||||
return True
|
||||
314
py12306/lib/helper.py
Normal file
314
py12306/lib/helper.py
Normal file
@@ -0,0 +1,314 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Coroutine, Any, Union
|
||||
from functools import wraps
|
||||
from lib.exceptions import RetryException, MaxRetryException
|
||||
|
||||
|
||||
def retry(num: int = 3):
|
||||
""" Retry a func """
|
||||
from app.app import Logger
|
||||
|
||||
__retry_num_key = '__retry_num'
|
||||
__call = None
|
||||
if hasattr(num, '__call__'):
|
||||
__call = num
|
||||
num = 3
|
||||
|
||||
def decorator(func):
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
retry_num = kwargs.get(__retry_num_key, num)
|
||||
try:
|
||||
if __retry_num_key in kwargs:
|
||||
del kwargs[__retry_num_key]
|
||||
return await func(*args, **kwargs)
|
||||
except RetryException as err:
|
||||
if retry_num > 0:
|
||||
kwargs[__retry_num_key] = retry_num - 1
|
||||
Logger.warning(
|
||||
f"重试 {func.__name__}{f',{err.msg}' if err.msg else ''}, 剩余次数 {kwargs[__retry_num_key]}")
|
||||
if err.wait_s:
|
||||
await asyncio.sleep(err.wait_s)
|
||||
return await async_wrapper(*args, **kwargs)
|
||||
if err.default:
|
||||
return err.default
|
||||
raise MaxRetryException(*err.args) from err
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
retry_num = kwargs.get(__retry_num_key, num)
|
||||
try:
|
||||
if __retry_num_key in kwargs:
|
||||
del kwargs[__retry_num_key]
|
||||
return func(*args, **kwargs)
|
||||
except RetryException as err:
|
||||
if retry_num > 0:
|
||||
kwargs[__retry_num_key] = retry_num - 1
|
||||
Logger.warning(
|
||||
f"重试 {func.__name__}{f',{err.msg}' if err.msg else ''}, 剩余次数 {kwargs[__retry_num_key]}")
|
||||
if err.wait_s:
|
||||
time.sleep(err.wait_s)
|
||||
return wrapper(*args, **kwargs)
|
||||
if err.default:
|
||||
return err.default
|
||||
raise MaxRetryException(*err.args) from err
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return wrapper
|
||||
|
||||
if __call:
|
||||
return decorator(__call)
|
||||
return decorator
|
||||
|
||||
|
||||
def number_of_time_period(period: str) -> int:
|
||||
"""
|
||||
Example: 23:00 -> 2300
|
||||
:param period:
|
||||
:return:
|
||||
"""
|
||||
return int(period.replace(':', ''))
|
||||
|
||||
|
||||
def md5(value):
|
||||
import hashlib
|
||||
return hashlib.md5(json.dumps(value).encode()).hexdigest()
|
||||
|
||||
|
||||
def run_async(coro: Coroutine):
|
||||
"""
|
||||
Simple async runner
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(coro)
|
||||
|
||||
|
||||
def json_encoder(obj: Any):
|
||||
""" JSON 序列化, 修复时间 """
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if isinstance(obj, datetime.date):
|
||||
return obj.strftime('%Y-%m-%d')
|
||||
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
def json_decoder(obj: Any):
|
||||
""" JSON 反序列化,加载时间 """
|
||||
ret = obj
|
||||
if isinstance(obj, list):
|
||||
obj = enumerate(obj)
|
||||
elif isinstance(obj, dict):
|
||||
obj = obj.items()
|
||||
else:
|
||||
return obj
|
||||
|
||||
for key, item in obj:
|
||||
if isinstance(item, (list, dict)):
|
||||
ret[key] = json_decoder(item)
|
||||
elif isinstance(item, str):
|
||||
try:
|
||||
if len(item) is 10:
|
||||
ret[key] = datetime.datetime.strptime(item, '%Y-%m-%d').date()
|
||||
else:
|
||||
ret[key] = datetime.datetime.strptime(item, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
ret[key] = item
|
||||
else:
|
||||
ret[key] = item
|
||||
return ret
|
||||
|
||||
|
||||
class JSONDecoder(json.JSONDecoder):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parse_array = self.__parse_array
|
||||
self.object_hook = json_decoder
|
||||
self.scan_once = json.scanner.py_make_scanner(self)
|
||||
|
||||
def __parse_array(self, *args, **kwargs):
|
||||
values, end = json.decoder.JSONArray(*args, **kwargs)
|
||||
return self.object_hook(values), end
|
||||
|
||||
|
||||
def json_friendly_loads(obj: Any, **kwargs):
|
||||
return json.loads(obj, cls=JSONDecoder, **kwargs)
|
||||
|
||||
|
||||
def json_friendly_dumps(obj: Any, **kwargs):
|
||||
return json.dumps(obj, ensure_ascii=False, default=json_encoder, **kwargs)
|
||||
|
||||
|
||||
def str_to_date(_str: str):
|
||||
if isinstance(_str, datetime.date):
|
||||
return _str
|
||||
if len(_str) is 10:
|
||||
return datetime.datetime.strptime(_str, '%Y-%m-%d').date()
|
||||
else:
|
||||
return datetime.datetime.strptime(_str, '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
def lmap(*args, **kwargs):
|
||||
return list(map(*args, **kwargs))
|
||||
|
||||
|
||||
class ShareInstance:
|
||||
__session = None
|
||||
|
||||
@classmethod
|
||||
def share(cls):
|
||||
if not cls.__session or cls is not cls.__session.__class__:
|
||||
cls.__session = cls()
|
||||
return cls.__session
|
||||
|
||||
|
||||
class SuperDict(dict):
|
||||
def get(self, key, default=None, sep='.'):
|
||||
keys = key.split(sep)
|
||||
for i, key in enumerate(keys):
|
||||
try:
|
||||
value = self[key]
|
||||
if len(keys[i + 1:]) and isinstance(value, SuperDict):
|
||||
return value.get(sep.join(keys[i + 1:]), default=default, sep=sep)
|
||||
return value
|
||||
except KeyError:
|
||||
return self.dict_to_dict(default)
|
||||
|
||||
def __getitem__(self, k):
|
||||
return self.dict_to_dict(super().__getitem__(k))
|
||||
|
||||
@classmethod
|
||||
def dict_to_dict(cls, value):
|
||||
return SuperDict(value) if isinstance(value, dict) else value
|
||||
|
||||
|
||||
class StationHelper(ShareInstance):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._stations = []
|
||||
self.version = ''
|
||||
from app.app import Config
|
||||
if os.path.exists(Config.DATA_DIR + 'stationList.json'):
|
||||
with open(Config.DATA_DIR + 'stationList.json', 'r', encoding='utf-8') as f:
|
||||
ret = json.load(f)
|
||||
self._stations = ret['stationList']
|
||||
self.version = ret['version_no']
|
||||
|
||||
@classmethod
|
||||
def stations(cls):
|
||||
return cls.share()._stations
|
||||
|
||||
@classmethod
|
||||
def cn_by_id(cls, _id: str):
|
||||
self = cls.share()
|
||||
for station in self._stations:
|
||||
if station.get('id') == _id:
|
||||
return station.get('value')
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def id_by_cn(cls, cn: str):
|
||||
for station in cls.share()._stations:
|
||||
if station.get('value') == cn:
|
||||
return station.get('id')
|
||||
return ''
|
||||
|
||||
|
||||
class UserTypeHelper:
|
||||
ADULT = 1
|
||||
CHILD = 2
|
||||
STUDENT = 3
|
||||
SOLDIER = 4
|
||||
|
||||
dicts = {
|
||||
ADULT: '成人',
|
||||
CHILD: '儿童',
|
||||
STUDENT: '学生',
|
||||
SOLDIER: '残疾军人、伤残人民警察'
|
||||
}
|
||||
|
||||
|
||||
class TrainSeat:
|
||||
NO_SEAT = 26
|
||||
|
||||
ticket = {'一等座': 'ZY', '二等座': 'ZE', '商务座': 'SWZ', '特等座': 'TZ', '硬座': 'YZ', '软座': 'RZ', '硬卧': 'YW', '二等卧': 'YW',
|
||||
'软卧': 'RW', '一等卧': 'RW', '高级软卧': 'GR', '动卧': 'SRRB', '高级动卧': 'YYRW', '无座': 'WZ'}
|
||||
ticket_id = {'一等座': 31, '二等座': 30, '商务座': 32, '特等座': 25, '硬座': 29, '软座': 24, '硬卧': 28, '二等卧': 28,
|
||||
'软卧': 23, '一等卧': 23, '高级软卧': 21, '动卧': 33, '高级动卧': -1, '无座': 26}
|
||||
order_id = {'棚车': '0', '硬座': '1', '软座': '2', '硬卧': '3', '软卧': '4', '包厢硬卧': '5', '高级软卧': '6', '一等软座': '7',
|
||||
'二等软座': '8', '商务座': '9', '高级动卧': 'A', '混编硬座': 'B', '混编硬卧': 'C', '包厢软座': 'D', '特等软座': 'E', '动卧': 'F',
|
||||
'二人软包': 'G', '一人软包': 'H', '一等卧': 'I', '二等卧': 'J', '混编软座': 'K', '混编软卧': 'L', '一等座': 'M', '二等座': 'O',
|
||||
'特等座': 'P', '观光座': 'Q', '一等包座': 'S', '无座': 'WZ'}
|
||||
|
||||
|
||||
class TaskManager(ABC, ShareInstance):
|
||||
__session = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.fuatures = []
|
||||
self.tasks = {}
|
||||
self.interval = 5
|
||||
|
||||
@abstractmethod
|
||||
async def run(self):
|
||||
""" """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
async def task_total(self) -> int:
|
||||
""" 任务总数 """
|
||||
pass
|
||||
|
||||
async def wait(self):
|
||||
if self.fuatures:
|
||||
await asyncio.wait(self.fuatures)
|
||||
|
||||
@property
|
||||
async def capacity_num(self) -> int:
|
||||
from app.app import Config
|
||||
if not Config.node_num:
|
||||
return 0
|
||||
return math.ceil((await self.task_total) / Config.node_num)
|
||||
|
||||
@property
|
||||
def task_num(self) -> int:
|
||||
return len(self.tasks)
|
||||
|
||||
def add_task(self, future, _id: Union[str, int], data: Any):
|
||||
future = asyncio.ensure_future(future)
|
||||
self.fuatures.append(future)
|
||||
self.tasks[_id] = data
|
||||
return self
|
||||
|
||||
@property
|
||||
async def is_full(self) -> bool:
|
||||
return self.task_num >= await self.capacity_num
|
||||
|
||||
@property
|
||||
async def is_overflow(self) -> bool:
|
||||
return self.task_num > await self.capacity_num
|
||||
|
||||
def get_task(self, _key, default=None):
|
||||
return self.tasks.get(_key, default)
|
||||
|
||||
def stop_and_drop(self, _key):
|
||||
task = self.tasks[_key]
|
||||
task.stop()
|
||||
del self.tasks[_key]
|
||||
|
||||
def clean_fuatures(self):
|
||||
[self.fuatures.remove(fut) for fut in self.fuatures if fut.done()]
|
||||
240
py12306/lib/request.py
Normal file
240
py12306/lib/request.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pickle
|
||||
import random
|
||||
from json import JSONDecoder
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from concurrent.futures._base import TimeoutError
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import TraceRequestStartParams, ClientSession, ClientResponse, ClientProxyConnectionError
|
||||
from app.models import QueryJob
|
||||
from lib.exceptions import RetryException
|
||||
from lib.helper import ShareInstance, SuperDict, run_async
|
||||
|
||||
|
||||
class ProxyHelepr(ShareInstance):
|
||||
|
||||
def __init__(self, config: Optional[dict] = None) -> None:
|
||||
super().__init__()
|
||||
from app.app import Config
|
||||
self._config: dict = config or Config.get('proxy')
|
||||
|
||||
async def get_proxy(self):
|
||||
if self._config.get('enable') is not True or not self._config.get('url'):
|
||||
return None
|
||||
response = await Session.share().get(self._config['url'])
|
||||
result = response.json()
|
||||
if result.get('http'):
|
||||
return result.get('http')
|
||||
return None
|
||||
|
||||
|
||||
class Session(ShareInstance):
|
||||
|
||||
def __init__(self, use_proxy: bool = False, timeout: int = 0):
|
||||
trace_config = aiohttp.TraceConfig()
|
||||
trace_config.on_request_start.append(self.on_request_start)
|
||||
trace_config.on_request_end.append(self.on_request_end)
|
||||
params = {}
|
||||
if timeout:
|
||||
params['timeout'] = aiohttp.ClientTimeout(total=timeout)
|
||||
self.session = aiohttp.ClientSession(trace_configs=[trace_config], **params)
|
||||
self.use_proxy = use_proxy
|
||||
self.api_base = ''
|
||||
|
||||
@classmethod
|
||||
def share_session(cls):
|
||||
self = cls.share()
|
||||
return self.session
|
||||
|
||||
async def on_request_start(self, session: ClientSession, trace_config_ctx: SimpleNamespace,
|
||||
params: TraceRequestStartParams):
|
||||
pass # TODO
|
||||
|
||||
async def on_request_end(self, session: ClientSession, trace_config_ctx: SimpleNamespace,
|
||||
params: TraceRequestStartParams):
|
||||
pass # TODO
|
||||
|
||||
async def overwrite_text(self, response, *args, **kwargs) -> Any:
|
||||
ret = await ClientResponse.text(response, *args, **kwargs)
|
||||
|
||||
def wrap():
|
||||
return ret
|
||||
|
||||
return wrap
|
||||
|
||||
def overwrite_json(self, response: ClientResponse) -> Any:
|
||||
def wrap(loads: JSONDecoder = json.loads):
|
||||
if hasattr(response, 'text_json'):
|
||||
return response.text_json
|
||||
try:
|
||||
load_dict = loads(response.text())
|
||||
except Exception:
|
||||
load_dict = {}
|
||||
ret = SuperDict.dict_to_dict(load_dict)
|
||||
response.text_json = ret
|
||||
return ret
|
||||
|
||||
return wrap
|
||||
|
||||
def cookie_dumps(self) -> str:
|
||||
return pickle.dumps(self.session.cookie_jar._cookies, False).decode()
|
||||
|
||||
def cookie_loads(self, cookies: str):
|
||||
self.session.cookie_jar._cookies = pickle.loads(cookies.encode())
|
||||
|
||||
def cookie_clean(self):
|
||||
self.session.cookie_jar.clear()
|
||||
|
||||
async def request(self, method: Any, url: str, headers=None, data: Any = None, use_proxy: bool = None,
|
||||
**kwargs: Any) -> ClientResponse:
|
||||
from app.app import Logger, Config
|
||||
proxy = kwargs.get('proxy', None)
|
||||
if proxy is None and (use_proxy is True or (use_proxy is None and self.use_proxy)):
|
||||
if random.randint(1, 100) <= Config.get('proxy.rate', 100):
|
||||
kwargs['proxy'] = await ProxyHelepr.share().get_proxy()
|
||||
# if kwargs.get('proxy'): # TODO move to event
|
||||
# Logger.debug(f"使用代理 {kwargs['proxy']}")
|
||||
default_headers = {
|
||||
'User-Agent': f'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
f'Chrome/78.0.{random.randint(3900, 3999)}.97 Safari/537.36'
|
||||
}
|
||||
if headers:
|
||||
default_headers.update(headers)
|
||||
parsed_url = urlparse(url)
|
||||
if not parsed_url.scheme:
|
||||
url = f'{self.api_base}{url}'
|
||||
try:
|
||||
response = await self.session.request(method, url, headers=default_headers, data=data, **kwargs)
|
||||
except Exception as e:
|
||||
err = f'{e.__class__.__qualname__} {method} {url}'
|
||||
if isinstance(e, TimeoutError):
|
||||
err = '请求超时 '
|
||||
if isinstance(e, ClientProxyConnectionError):
|
||||
err = f"代理连接失败 {kwargs.get('proxy')}"
|
||||
Logger.error(f'请求错误, {err}')
|
||||
raise RetryException(e, wait_s=1)
|
||||
response.text = await self.overwrite_text(response)
|
||||
response.json = self.overwrite_json(response)
|
||||
return response
|
||||
|
||||
async def get(self, url: str, headers=None, use_proxy: bool = None, **kwargs: Any) -> ClientResponse:
|
||||
return await self.request('GET', url=url, headers=headers, use_proxy=use_proxy, **kwargs)
|
||||
|
||||
async def post(self, url: str, headers=None, data: Any = None, use_proxy: bool = None,
|
||||
**kwargs: Any) -> ClientResponse:
|
||||
return await self.request('POST', url=url, headers=headers, data=data, use_proxy=use_proxy, **kwargs)
|
||||
|
||||
async def identify_captcha(self, captcha_image64: str):
|
||||
data = {
|
||||
'img': captcha_image64
|
||||
}
|
||||
# TODO timeout
|
||||
return await self.request('POST', f'https://12306-ocr.pjialin.com/check/', data=data)
|
||||
|
||||
async def browser_device_id_url(self):
|
||||
return await self.request('GET', f'https://12306-rail-id-v2.pjialin.com/')
|
||||
|
||||
def __del__(self):
|
||||
from app.app import Config
|
||||
if Config.IS_IN_TEST:
|
||||
run_async(self.session.close())
|
||||
else:
|
||||
asyncio.ensure_future(self.session.close())
|
||||
|
||||
|
||||
class TrainSession(Session):
|
||||
""" 请求处理类 """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_base = 'https://kyfw.12306.cn'
|
||||
|
||||
async def otn_left_ticket_init(self):
|
||||
return await self.request('GET', '/otn/leftTicket/init', use_proxy=False)
|
||||
|
||||
async def otn_query_left_ticket(self, api_type: str, queryjob: QueryJob):
|
||||
query = {'leftTicketDTO.train_date': queryjob.left_date, 'leftTicketDTO.from_station': queryjob.left_station_id,
|
||||
'leftTicketDTO.to_station': queryjob.arrive_station_id, 'purpose_codes': 'ADULT'}
|
||||
url = f'/otn/{api_type}?{urlencode(query)}'
|
||||
return await self.request('GET', url, allow_redirects=False)
|
||||
|
||||
async def passport_captcha_image64(self):
|
||||
rand_str = random.random()
|
||||
return await self.request('GET',
|
||||
f'/passport/captcha/captcha-image64?login_site=E&module=login&rand=sjrand&_={rand_str}')
|
||||
|
||||
async def passport_captcha_check(self, answer: str):
|
||||
""" 验证码校验 """
|
||||
rand_str = random.random()
|
||||
return await self.request('GET',
|
||||
f'/passport/captcha/captcha-check?answer={answer}&rand=sjrand&login_site=E&_={rand_str}')
|
||||
|
||||
async def browser_device_id(self, url: str):
|
||||
""" 获取浏览器特征 ID """
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
|
||||
}
|
||||
return await self.request('GET', url, headers=headers)
|
||||
|
||||
async def passport_web_login(self, data: dict):
|
||||
""" 登录 """
|
||||
return await self.request('POST', '/passport/web/login', data=data)
|
||||
|
||||
async def passport_web_auth_uamtk(self):
|
||||
""" 登录获取 uamtk """
|
||||
data = {'appid': 'otn'}
|
||||
headers = {
|
||||
'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin',
|
||||
'Origin': 'https://kyfw.12306.cn'
|
||||
}
|
||||
return await self.request('POST', '/passport/web/auth/uamtk', data=data, headers=headers)
|
||||
|
||||
async def otn_uamauthclient(self, uamtk: str):
|
||||
""" 登录获取 username """
|
||||
data = {'tk': uamtk}
|
||||
return await self.request('POST', '/otn/uamauthclient', data=data)
|
||||
|
||||
async def otn_modify_user_init_query_user_info(self):
|
||||
""" 获取用户详情信息 """
|
||||
return await self.request('GET', '/otn/modifyUser/initQueryUserInfoApi')
|
||||
|
||||
async def otn_confirm_passenger_get_passenger(self):
|
||||
""" 获取乘客列表 """
|
||||
return await self.request('POST', '/otn/confirmPassenger/getPassengerDTOs')
|
||||
|
||||
async def otn_login_conf(self):
|
||||
""" 获取登录状态 """
|
||||
return await self.request('GET', '/otn/login/conf')
|
||||
|
||||
async def otn_left_ticket_submit_order_request(self, data: dict):
|
||||
""" 提交下单请求 """
|
||||
return await self.request('POST', '/otn/leftTicket/submitOrderRequest', data=data)
|
||||
|
||||
async def otn_confirm_passenger_init_dc(self):
|
||||
""" 获取下单 token """
|
||||
data = {'_json_att': ''}
|
||||
return await self.request('POST', '/otn/confirmPassenger/initDc', data=data)
|
||||
|
||||
async def otn_confirm_passenger_check_order_info(self, data: dict):
|
||||
""" 检查下单信息 """
|
||||
return await self.request('POST', '/otn/confirmPassenger/checkOrderInfo', data=data)
|
||||
|
||||
async def otn_confirm_passenger_get_queue_count(self, data: dict):
|
||||
""" 检查下单信息 """
|
||||
return await self.request('POST', '/otn/confirmPassenger/getQueueCount', data=data)
|
||||
|
||||
async def otn_confirm_passenger_confirm_single_for_queue(self, data: dict):
|
||||
""" 确认排队 """
|
||||
return await self.request('POST', '/otn/confirmPassenger/confirmSingleForQueue', data=data)
|
||||
|
||||
async def otn_confirm_passenger_query_order_wait_time(self, querys: dict):
|
||||
""" 查询排队结果 """
|
||||
return await self.request('GET', f'/otn/confirmPassenger/queryOrderWaitTime?{urlencode(querys)}')
|
||||
|
||||
async def otn_query_my_order_no_complete(self):
|
||||
""" 查询未完成订单 """
|
||||
return await self.request('POST', f'/otn/queryOrder/queryMyOrderNoComplete', data={'_json_att': ''})
|
||||
@@ -1,77 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
class BaseLog:
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
@classmethod
|
||||
def add_log(cls, content=''):
|
||||
self = cls()
|
||||
# print('添加 Log 主进程{} 进程ID{}'.format(is_main_thread(), current_thread_id()))
|
||||
if is_main_thread():
|
||||
self.logs.append(content)
|
||||
else:
|
||||
tmp_log = self.thread_logs.get(current_thread_id(), [])
|
||||
tmp_log.append(content)
|
||||
self.thread_logs[current_thread_id()] = tmp_log
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def flush(cls, sep='\n', end='\n', file=None, exit=False, publish=True):
|
||||
from py12306.cluster.cluster import Cluster
|
||||
self = cls()
|
||||
logs = self.get_logs()
|
||||
# 输出到文件
|
||||
if file == None and Config().OUT_PUT_LOG_TO_FILE_ENABLED and not Const.IS_TEST: # TODO 文件无法写入友好提示
|
||||
file = open(Config().OUT_PUT_LOG_TO_FILE_PATH, 'a', encoding='utf-8')
|
||||
if not file: file = None
|
||||
# 输出日志到各个节点
|
||||
if publish and self.quick_log and Config().is_cluster_enabled() and Cluster().is_ready: #
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
print(*logs, sep=sep, end='' if end == '\n' else end)
|
||||
out = f.getvalue()
|
||||
Cluster().publish_log_message(out)
|
||||
else:
|
||||
print(*logs, sep=sep, end=end, file=file)
|
||||
self.empty_logs(logs)
|
||||
if exit: sys.exit()
|
||||
|
||||
def get_logs(self):
|
||||
if self.quick_log:
|
||||
logs = self.quick_log
|
||||
else:
|
||||
if is_main_thread():
|
||||
logs = self.logs
|
||||
else:
|
||||
logs = self.thread_logs.get(current_thread_id())
|
||||
return logs
|
||||
|
||||
def empty_logs(self, logs=None):
|
||||
if self.quick_log:
|
||||
self.quick_log = []
|
||||
else:
|
||||
if is_main_thread():
|
||||
self.logs = []
|
||||
else:
|
||||
if logs and self.thread_logs.get(current_thread_id()): del self.thread_logs[current_thread_id()]
|
||||
|
||||
@classmethod
|
||||
def add_quick_log(cls, content=''):
|
||||
self = cls()
|
||||
self.quick_log.append(content)
|
||||
return self
|
||||
|
||||
def notification(self, title, content=''):
|
||||
# if sys.platform == 'darwin': # 不太友好 先关闭,之前没考虑到 mac 下会请求权限
|
||||
# os.system( 'osascript -e \'tell app "System Events" to display notification "{content}" with title "{title}"\''.format(
|
||||
# title=title, content=content))
|
||||
pass
|
||||
@@ -1,35 +0,0 @@
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class ClusterLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
MESSAGE_JOIN_CLUSTER_SUCCESS = '# 节点 {} 成功加入到集群,当前节点列表 {} #'
|
||||
|
||||
MESSAGE_LEFT_CLUSTER = '# 节点 {} 已离开集群,当前节点列表 {} #'
|
||||
|
||||
MESSAGE_NODE_ALREADY_IN_CLUSTER = '# 当前节点已存在于集群中,自动分配新的节点名称 {} #'
|
||||
|
||||
MESSAGE_SUBSCRIBE_NOTIFICATION_PREFIX = '{} )'
|
||||
MESSAGE_SUBSCRIBE_NOTIFICATION = MESSAGE_SUBSCRIBE_NOTIFICATION_PREFIX + '{}'
|
||||
|
||||
MESSAGE_ASCENDING_MASTER_NODE = '# 已将 {} 提升为主节点,当前节点列表 {} #'
|
||||
|
||||
MESSAGE_MASTER_DID_LOST = '# 主节点已退出,{} 秒后程序将自动退出 #'
|
||||
|
||||
MESSAGE_MASTER_NODE_ALREADY_RUN = '# 启动失败,主节点 {} 已经在运行中 #'
|
||||
MESSAGE_MASTER_NODE_NOT_FOUND = '# 启动失败,请先启动主节点 #'
|
||||
|
||||
MESSAGE_NODE_BECOME_MASTER_AGAIN = '# 节点 {} 已启动,已自动成为主节点 #'
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_print_nodes(nodes):
|
||||
message = ['{}{}'.format('*' if val == '1' else '', key) for key, val in nodes.items()]
|
||||
return '[ {} ]'.format(', '.join(message))
|
||||
@@ -1,148 +0,0 @@
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.config import *
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class CommonLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
MESSAGE_12306_IS_CLOSED = '当前时间: {} | 12306 休息时间,程序将在明天早上 6 点自动运行'
|
||||
MESSAGE_RETRY_AUTH_CODE = '{} 秒后重新获取验证码'
|
||||
|
||||
MESSAGE_EMPTY_APP_CODE = '无法发送语音消息,未填写验证码接口 appcode'
|
||||
MESSAGE_VOICE_API_FORBID = '语音消息发送失败,请检查 appcode 是否填写正确或 套餐余额是否充足'
|
||||
MESSAGE_VOICE_API_SEND_FAIL = '语音消息发送失败,错误原因 {}'
|
||||
MESSAGE_VOICE_API_SEND_SUCCESS = '语音消息发送成功! 接口返回信息 {} '
|
||||
|
||||
MESSAGE_CHECK_AUTO_CODE_FAIL = '请配置打码账号的账号密码'
|
||||
MESSAGE_CHECK_EMPTY_USER_ACCOUNT = '请配置 12306 账号密码'
|
||||
|
||||
MESSAGE_TEST_SEND_VOICE_CODE = '正在测试发送语音验证码...'
|
||||
MESSAGE_TEST_SEND_EMAIL = '正在测试发送邮件...'
|
||||
MESSAGE_TEST_SEND_DINGTALK = '正在测试发送钉钉消息...'
|
||||
MESSAGE_TEST_SEND_TELEGRAM = '正在测试推送到Telegram...'
|
||||
MESSAGE_TEST_SEND_SERVER_CHAN = '正在测试发送ServerChan消息...'
|
||||
MESSAGE_TEST_SEND_PUSH_BEAR = '正在测试发送PushBear消息...'
|
||||
MESSAGE_TEST_SEND_PUSH_BARK = '正在测试发送Bark消息...'
|
||||
|
||||
MESSAGE_CONFIG_FILE_DID_CHANGED = '配置文件已修改,正在重新加载中\n'
|
||||
MESSAGE_API_RESPONSE_CAN_NOT_BE_HANDLE = '接口返回错误'
|
||||
|
||||
MESSAGE_SEND_EMAIL_SUCCESS = '邮件发送成功,请检查收件箱'
|
||||
MESSAGE_SEND_EMAIL_FAIL = '邮件发送失败,请手动检查配置,错误原因 {}'
|
||||
|
||||
MESSAGE_SEND_TELEGRAM_SUCCESS = 'Telegram推送成功'
|
||||
MESSAGE_SEND_TELEGRAM_FAIL = 'Telegram推送失败,错误原因 {}'
|
||||
|
||||
MESSAGE_SEND_SERVER_CHAN_SUCCESS = '发送成功,请检查微信'
|
||||
MESSAGE_SEND_SERVER_CHAN_FAIL = 'ServerChan发送失败,请检查KEY'
|
||||
|
||||
MESSAGE_SEND_PUSH_BEAR_SUCCESS = '发送成功,请检查微信'
|
||||
MESSAGE_SEND_PUSH_BEAR_FAIL = 'PushBear发送失败,请检查KEY'
|
||||
|
||||
MESSAGE_SEND_BARK_SUCCESS = 'Bark推送成功'
|
||||
MESSAGE_SEND_BARK_FAIL = 'Bark推送失败,错误原因 {}'
|
||||
|
||||
MESSAGE_OUTPUT_TO_FILE_IS_UN_ENABLE = '请先打开配置项中的:OUT_PUT_LOG_TO_FILE_ENABLED ( 输出到文件 )'
|
||||
|
||||
MESSAGE_GET_RESPONSE_FROM_FREE_AUTO_CODE = '从免费打码获取结果失败'
|
||||
|
||||
MESSAGE_RESPONSE_EMPTY_ERROR = '网络错误'
|
||||
|
||||
MESSAGE_CDN_START_TO_CHECK = '正在筛选 {} 个 CDN...'
|
||||
MESSAGE_CDN_START_TO_RECHECK = '正在重新筛选 {} 个 CDN...当前时间 {}\n'
|
||||
MESSAGE_CDN_RESTORE_SUCCESS = 'CDN 恢复成功,上次检测 {}\n'
|
||||
MESSAGE_CDN_CHECKED_SUCCESS = '# CDN 检测完成,可用 CDN {} #\n'
|
||||
MESSAGE_CDN_CLOSED = '# CDN 已关闭 #'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.init_data()
|
||||
|
||||
def init_data(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def print_welcome(cls):
|
||||
self = cls()
|
||||
self.add_quick_log('######## py12306 购票助手,本程序为开源工具,请勿用于商业用途 ########')
|
||||
if Const.IS_TEST:
|
||||
self.add_quick_log()
|
||||
self.add_quick_log('当前为测试模式,程序运行完成后自动结束')
|
||||
if not Const.IS_TEST and Config().OUT_PUT_LOG_TO_FILE_ENABLED:
|
||||
self.add_quick_log()
|
||||
self.add_quick_log('日志已输出到文件中: {}'.format(Config().OUT_PUT_LOG_TO_FILE_PATH))
|
||||
if Config().WEB_ENABLE:
|
||||
self.add_quick_log()
|
||||
self.add_quick_log('WEB 管理页面已开启,请访问 主机地址 + 端口 {} 进行查看'.format(Config().WEB_PORT))
|
||||
|
||||
self.add_quick_log()
|
||||
self.flush(file=False, publish=False)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_configs(cls):
|
||||
# 打印配置
|
||||
self = cls()
|
||||
enable = '已开启'
|
||||
disable = '未开启'
|
||||
self.add_quick_log('**** 当前配置 ****')
|
||||
self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable)))
|
||||
self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush()
|
||||
self.add_quick_log('通知状态:')
|
||||
if Config().NOTIFICATION_BY_VOICE_CODE:
|
||||
self.add_quick_log(
|
||||
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
|
||||
if Config().EMAIL_ENABLED:
|
||||
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
|
||||
if Config().DINGTALK_ENABLED:
|
||||
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
|
||||
if Config().TELEGRAM_ENABLED:
|
||||
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
|
||||
if Config().SERVERCHAN_ENABLED:
|
||||
self.add_quick_log(
|
||||
'ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
|
||||
if Config().BARK_ENABLED:
|
||||
self.add_quick_log('Bark通知: {}'.format(get_true_false_text(Config().BARK_ENABLED, enable, disable)))
|
||||
if Config().PUSHBEAR_ENABLED:
|
||||
self.add_quick_log(
|
||||
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable)))
|
||||
self.add_quick_log().flush(sep='\t\t')
|
||||
self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL))
|
||||
self.add_quick_log('用户心跳检测间隔: {} 秒'.format(Config().USER_HEARTBEAT_INTERVAL))
|
||||
self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable)))
|
||||
if Config().is_cluster_enabled():
|
||||
from py12306.cluster.cluster import Cluster
|
||||
self.add_quick_log('分布式查询: {}'.format(get_true_false_text(Config().is_cluster_enabled(), enable, enable)))
|
||||
self.add_quick_log('节点名称: {}'.format(Cluster().node_name))
|
||||
self.add_quick_log('节点是否主节点: {}'.format(get_true_false_text(Config().is_master(), '是', '否')))
|
||||
self.add_quick_log(
|
||||
'子节点提升为主节点: {}'.format(get_true_false_text(Config().NODE_SLAVE_CAN_BE_MASTER, enable, disable)))
|
||||
self.add_quick_log()
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_test_complete(cls):
|
||||
self = cls()
|
||||
self.add_quick_log('# 测试完成,请检查输出是否正确 #')
|
||||
self.flush(publish=False)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_auto_code_fail(cls, reason):
|
||||
self = cls()
|
||||
self.add_quick_log('打码失败: 错误原因 {reason}'.format(reason=reason))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_auth_code_info(cls, reason):
|
||||
self = cls()
|
||||
self.add_quick_log('打码信息: {reason}'.format(reason=reason))
|
||||
self.flush()
|
||||
return self
|
||||
@@ -1,72 +0,0 @@
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class OrderLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
MESSAGE_REQUEST_INIT_DC_PAGE_FAIL = '请求初始化订单页面失败'
|
||||
|
||||
MESSAGE_SUBMIT_ORDER_REQUEST_FAIL = '提交订单失败,错误原因 {} \n'
|
||||
MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS = '提交订单成功'
|
||||
MESSAGE_CHECK_ORDER_INFO_FAIL = '检查订单失败,错误原因 {} \n'
|
||||
MESSAGE_CHECK_ORDER_INFO_SUCCESS = '检查订单成功'
|
||||
|
||||
MESSAGE_GET_QUEUE_INFO_SUCCESS = '获取排队信息成功,目前排队人数 {}, 余票还剩余 {} 张'
|
||||
MESSAGE_GET_QUEUE_INFO_NO_SEAT = '接口返回实际为无票,跳过本次排队'
|
||||
MESSAGE_GET_QUEUE_COUNT_SUCCESS = '排队成功,你当前排在第 {} 位, 余票还剩余 {} 张'
|
||||
MESSAGE_GET_QUEUE_LESS_TICKET = '排队失败,目前排队人数已经超过余票张数'
|
||||
MESSAGE_GET_QUEUE_COUNT_FAIL = '排队失败,错误原因 {}'
|
||||
|
||||
MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_SUCCESS = '# 提交订单成功!#'
|
||||
MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_ERROR = '出票失败,错误原因 {}'
|
||||
MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_FAIL = '提交订单失败,错误原因 {}'
|
||||
|
||||
MESSAGE_QUERY_ORDER_WAIT_TIME_WAITING = '排队等待中,排队人数 {},预计还需要 {} 秒'
|
||||
MESSAGE_QUERY_ORDER_WAIT_TIME_FAIL = '排队失败,错误原因 {}'
|
||||
MESSAGE_QUERY_ORDER_WAIT_TIME_INFO = '第 {} 次排队,请耐心等待'
|
||||
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE = '车票购买成功!'
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_CONTENT = '请及时登录12306账号[{}],打开 \'未完成订单\',在30分钟内完成支付!'
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_INFO = '\t\t车次信息: {} {}[{}] -> {}[{}],乘车日期 {},席位:{},乘车人:{}'
|
||||
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_VOICE_CODE_START_SEND = '正在发送语音通知...'
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_VOICE_CODE_CONTENT = '你的车票 {} 到 {} 购买成功,请登录 12306 进行支付'
|
||||
|
||||
MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT = '订单号 {},请及时登录12306账号[{}],打开 \'未完成订单\',在30分钟内完成支付!'
|
||||
|
||||
MESSAGE_JOB_CLOSED = '当前任务已结束'
|
||||
|
||||
@classmethod
|
||||
def print_passenger_did_deleted(cls, passengers):
|
||||
self = cls()
|
||||
result = [passenger.get('name') + '(' + passenger.get('type_text') + ')' for passenger in passengers]
|
||||
self.add_quick_log('# 删减后的乘客列表 {} #'.format(', '.join(result)))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_ticket_did_ordered(cls, order_id):
|
||||
self = cls()
|
||||
self.add_quick_log('# 车票购买成功,订单号 {} #'.format(order_id))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def get_order_success_notification_info(cls, query):
|
||||
from py12306.query.job import Job
|
||||
assert isinstance(query, Job)
|
||||
passengers = [passenger.get(
|
||||
'name') + '(' + passenger.get('type_text') + ')' for passenger in query.passengers]
|
||||
return cls.MESSAGE_ORDER_SUCCESS_NOTIFICATION_INFO.format(query.get_info_of_train_number(),
|
||||
query.get_info_of_left_station(),
|
||||
query.get_info_of_train_left_time(),
|
||||
query.get_info_of_arrive_station(),
|
||||
query.get_info_of_train_arrive_time(),
|
||||
query.get_info_of_left_date(),
|
||||
query.current_seat_name,
|
||||
','.join(passengers))
|
||||
@@ -1,200 +0,0 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
from os import path
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class QueryLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
data = {
|
||||
'query_count': 0,
|
||||
'last_time': '',
|
||||
}
|
||||
data_path = None
|
||||
|
||||
LOG_INIT_JOBS = ''
|
||||
|
||||
MESSAGE_GIVE_UP_CHANCE_CAUSE_TICKET_NUM_LESS_THAN_SPECIFIED = '余票数小于乘车人数,放弃此次提交机会'
|
||||
MESSAGE_QUERY_LOG_OF_EVERY_TRAIN = '{}'
|
||||
MESSAGE_QUERY_LOG_OF_TRAIN_INFO = '{} {}'
|
||||
MESSAGE_QUERY_START_BY_DATE = '出发日期 {}: {} - {}'
|
||||
|
||||
MESSAGE_JOBS_DID_CHANGED = '任务已更新,正在重新加载...\n'
|
||||
|
||||
MESSAGE_SKIP_ORDER = '跳过本次请求,节点 {} 用户 {} 正在处理该订单\n'
|
||||
|
||||
MESSAGE_QUERY_JOB_BEING_DESTROY = '查询任务 {} 已结束\n'
|
||||
|
||||
MESSAGE_INIT_PASSENGERS_SUCCESS = '初始化乘客成功'
|
||||
MESSAGE_CHECK_PASSENGERS = '查询任务 {} 正在验证乘客信息'
|
||||
|
||||
MESSAGE_USER_IS_EMPTY_WHEN_DO_ORDER = '未配置自动下单账号,{} 秒后继续查询\n'
|
||||
MESSAGE_ORDER_USER_IS_EMPTY = '未找到下单账号,{} 秒后继续查询'
|
||||
|
||||
cluster = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.data_path = Config().QUERY_DATA_DIR + 'status.json'
|
||||
self.cluster = Cluster()
|
||||
|
||||
@classmethod
|
||||
def init_data(cls):
|
||||
self = cls()
|
||||
# 获取上次记录
|
||||
result = False
|
||||
if not Config.is_cluster_enabled() and path.exists(self.data_path):
|
||||
with open(self.data_path, encoding='utf-8') as f:
|
||||
result = f.read()
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except json.JSONDecodeError as e:
|
||||
result = {}
|
||||
# self.add_quick_log('加载status.json失败, 文件内容为: {}.'.format(repr(result)))
|
||||
# self.flush() # 这里可以用不用提示
|
||||
|
||||
if Config.is_cluster_enabled():
|
||||
result = self.get_data_from_cluster()
|
||||
|
||||
if result:
|
||||
self.data = {**self.data, **result}
|
||||
self.print_data_restored()
|
||||
|
||||
def get_data_from_cluster(self):
|
||||
query_count = self.cluster.session.get(Cluster.KEY_QUERY_COUNT, 0)
|
||||
last_time = self.cluster.session.get(Cluster.KEY_QUERY_LAST_TIME, '')
|
||||
if query_count and last_time:
|
||||
return {'query_count': query_count, 'last_time': last_time}
|
||||
return False
|
||||
|
||||
def refresh_data_of_cluster(self):
|
||||
return {
|
||||
'query_count': self.cluster.session.incr(Cluster.KEY_QUERY_COUNT),
|
||||
'last_time': self.cluster.session.set(Cluster.KEY_QUERY_LAST_TIME, time_now()),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def print_init_jobs(cls, jobs):
|
||||
"""
|
||||
输出初始化信息
|
||||
:return:
|
||||
"""
|
||||
self = cls()
|
||||
self.add_log('# 发现 {} 个任务 #'.format(len(jobs)))
|
||||
index = 1
|
||||
for job in jobs:
|
||||
self.add_log('================== 任务 {} =================='.format(index))
|
||||
for station in job.stations:
|
||||
self.add_log('出发站:{} 到达站:{}'.format(station.get('left'), station.get('arrive')))
|
||||
|
||||
self.add_log('乘车日期:{}'.format(job.left_dates))
|
||||
self.add_log('坐席:{}'.format(','.join(job.allow_seats)))
|
||||
self.add_log('乘车人:{}'.format(','.join(job.members)))
|
||||
if job.except_train_numbers:
|
||||
train_number_message = '排除 ' + ','.join(job.allow_train_numbers)
|
||||
else:
|
||||
train_number_message = ','.join(job.allow_train_numbers if job.allow_train_numbers else ['不筛选'])
|
||||
self.add_log('筛选车次:{}'.format(train_number_message))
|
||||
self.add_log('任务名称:{}'.format(job.job_name))
|
||||
# 乘车日期:['2019-01-24', '2019-01-25', '2019-01-26', '2019-01-27']
|
||||
self.add_log('')
|
||||
index += 1
|
||||
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_ticket_num_less_than_specified(cls, rest_num, job):
|
||||
self = cls()
|
||||
self.add_quick_log(
|
||||
'余票数小于乘车人数,当前余票数: {rest_num}, 实际人数 {actual_num}, 删减人车人数到: {take_num}'.format(rest_num=rest_num,
|
||||
actual_num=job.member_num,
|
||||
take_num=job.member_num_take))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_ticket_seat_available(cls, left_date, train_number, seat_type, rest_num):
|
||||
self = cls()
|
||||
self.add_quick_log(
|
||||
'[ 查询到座位可用 出发时间 {left_date} 车次 {train_number} 座位类型 {seat_type} 余票数量 {rest_num} ]'.format(
|
||||
left_date=left_date,
|
||||
train_number=train_number,
|
||||
seat_type=seat_type,
|
||||
rest_num=rest_num))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_ticket_available(cls, left_date, train_number, rest_num):
|
||||
self = cls()
|
||||
self.add_quick_log('检查完成 开始提交订单 '.format())
|
||||
self.notification('查询到可用车票', '时间 {left_date} 车次 {train_number} 余票数量 {rest_num}'.format(left_date=left_date,
|
||||
train_number=train_number,
|
||||
rest_num=rest_num))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_query_error(cls, reason, code=None):
|
||||
self = cls()
|
||||
self.add_quick_log('查询余票请求失败')
|
||||
if code:
|
||||
self.add_quick_log('状态码 {} '.format(code))
|
||||
if reason:
|
||||
self.add_quick_log('错误原因 {} '.format(reason))
|
||||
self.flush(sep='\t')
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_job_start(cls, job_name):
|
||||
self = cls()
|
||||
message = '>> 第 {query_count} 次查询 {job_name} {time}'.format(
|
||||
query_count=int(self.data.get('query_count', 0)) + 1,
|
||||
job_name=job_name, time=time_now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
self.add_log(message)
|
||||
self.refresh_data()
|
||||
if is_main_thread():
|
||||
self.flush(publish=False)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def add_query_time_log(cls, time, is_cdn):
|
||||
return cls().add_log(('*' if is_cdn else '') + '耗时 %.2f' % time)
|
||||
|
||||
@classmethod
|
||||
def add_stay_log(cls, second):
|
||||
self = cls()
|
||||
self.add_log('停留 {}'.format(second))
|
||||
return self
|
||||
|
||||
def print_data_restored(self):
|
||||
self.add_quick_log('============================================================')
|
||||
self.add_quick_log('|=== 查询记录恢复成功 上次查询 {last_date} ===|'.format(last_date=self.data.get('last_time')))
|
||||
self.add_quick_log('============================================================')
|
||||
self.add_quick_log('')
|
||||
self.flush(publish=False)
|
||||
return self
|
||||
|
||||
def refresh_data(self):
|
||||
if Config.is_cluster_enabled():
|
||||
self.data = {**self.data, **self.refresh_data_of_cluster()}
|
||||
else:
|
||||
self.data['query_count'] += 1
|
||||
self.data['last_time'] = str(datetime.datetime.now())
|
||||
self.save_data()
|
||||
|
||||
def save_data(self):
|
||||
with open(self.data_path, 'w') as file:
|
||||
file.write(json.dumps(self.data))
|
||||
@@ -1,12 +0,0 @@
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class RedisLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
MESSAGE_REDIS_INIT_SUCCESS = 'Redis 初始化成功'
|
||||
@@ -1,76 +0,0 @@
|
||||
from py12306.log.base import BaseLog
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class UserLog(BaseLog):
|
||||
# 这里如果不声明,会出现重复打印,目前不知道什么原因
|
||||
logs = []
|
||||
thread_logs = {}
|
||||
quick_log = []
|
||||
|
||||
MESSAGE_DOWNLAOD_AUTH_CODE_FAIL = '验证码下载失败 错误原因: {} {} 秒后重试'
|
||||
MESSAGE_DOWNLAODING_THE_CODE = '正在下载验证码...'
|
||||
MESSAGE_CODE_AUTH_FAIL = '验证码验证失败 错误原因: {}'
|
||||
MESSAGE_CODE_AUTH_SUCCESS = '验证码验证成功 开始登录...'
|
||||
MESSAGE_LOGIN_FAIL = '登录失败 错误原因: {}'
|
||||
MESSAGE_LOADED_USER = '正在尝试恢复用户: {}'
|
||||
MESSAGE_LOADED_USER_SUCCESS = '用户恢复成功: {}'
|
||||
MESSAGE_LOADED_USER_BUT_EXPIRED = '用户状态已过期,正在重新登录'
|
||||
MESSAGE_USER_HEARTBEAT_NORMAL = '用户 {} 心跳正常,下次检测 {} 秒后'
|
||||
|
||||
MESSAGE_GET_USER_PASSENGERS_FAIL = '获取用户乘客列表失败,错误原因: {} {} 秒后重试'
|
||||
MESSAGE_USER_PASSENGERS_IS_INVALID = '乘客信息校验失败,在账号 {} 中未找到该乘客: {}\n'
|
||||
|
||||
# MESSAGE_WAIT_USER_INIT_COMPLETE = '未找到可用账号或用户正在初始化,{} 秒后重试'
|
||||
|
||||
MESSAGE_USERS_DID_CHANGED = '\n用户信息已更新,正在重新加载...'
|
||||
|
||||
MESSAGE_USER_BEING_DESTROY = '用户 {} 已退出'
|
||||
MESSAGE_USER_COOKIE_NOT_FOUND_FROM_REMOTE = '用户 {} 状态加载中...'
|
||||
|
||||
MESSAGE_WAIT_USER_INIT_COMPLETE = '账号正在登录中,{} 秒后自动重试'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.init_data()
|
||||
|
||||
def init_data(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def print_init_users(cls, users):
|
||||
"""
|
||||
输出初始化信息
|
||||
:return:
|
||||
"""
|
||||
self = cls()
|
||||
self.add_quick_log('# 发现 {} 个用户 #\n'.format(len(users)))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_welcome_user(cls, user):
|
||||
self = cls()
|
||||
self.add_quick_log('# 欢迎回来,{} #\n'.format(user.get_name()))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_start_login(cls, user):
|
||||
self = cls()
|
||||
self.add_quick_log('正在登录用户 {}'.format(user.user_name))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_user_passenger_init_success(cls, passengers):
|
||||
self = cls()
|
||||
result = [passenger.get('name') + '(' + passenger.get('type_text') + ')' for passenger in passengers]
|
||||
self.add_quick_log('# 乘客验证成功 {} #\n'.format(', '.join(result)))
|
||||
self.flush()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def print_user_expired(cls):
|
||||
return cls().add_quick_log(cls.MESSAGE_LOADED_USER_BUT_EXPIRED).flush()
|
||||
@@ -1,456 +0,0 @@
|
||||
import urllib
|
||||
|
||||
# from py12306.config import UserType
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import *
|
||||
from py12306.helpers.func import *
|
||||
from py12306.helpers.notification import Notification
|
||||
from py12306.helpers.type import UserType, SeatType
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.log.order_log import OrderLog
|
||||
|
||||
|
||||
class Order:
|
||||
"""
|
||||
处理下单
|
||||
"""
|
||||
session = None
|
||||
query_ins = None
|
||||
user_ins = None
|
||||
|
||||
passenger_ticket_str = ''
|
||||
old_passenger_str = ''
|
||||
|
||||
is_need_auth_code = False
|
||||
|
||||
max_queue_wait = 60 * 5 # 最大排队时长
|
||||
current_queue_wait = 0
|
||||
retry_time = 3
|
||||
wait_queue_interval = 3
|
||||
|
||||
order_id = 0
|
||||
|
||||
notification_sustain_time = 60 * 30 # 通知持续时间 30 分钟
|
||||
notification_interval = 5 * 60 # 通知间隔
|
||||
|
||||
def __init__(self, query, user):
|
||||
self.session = user.session
|
||||
from py12306.query.job import Job
|
||||
from py12306.user.job import UserJob
|
||||
assert isinstance(query, Job)
|
||||
assert isinstance(user, UserJob)
|
||||
self.query_ins = query
|
||||
self.user_ins = user
|
||||
|
||||
self.make_passenger_ticket_str()
|
||||
|
||||
def order(self):
|
||||
"""
|
||||
开始下单
|
||||
下单模式 暂时不清楚,使用正常步骤下单
|
||||
:return:
|
||||
"""
|
||||
# Debug
|
||||
if Config().IS_DEBUG:
|
||||
self.order_id = 'test'
|
||||
self.order_did_success()
|
||||
return random.randint(0, 10) > 7
|
||||
return self.normal_order()
|
||||
|
||||
def normal_order(self):
|
||||
order_request_res = self.submit_order_request()
|
||||
if order_request_res == -1:
|
||||
return self.order_did_success()
|
||||
elif not order_request_res:
|
||||
return
|
||||
if not self.user_ins.request_init_dc_page():
|
||||
return
|
||||
if not self.check_order_info():
|
||||
return
|
||||
if not self.get_queue_count():
|
||||
return
|
||||
if not self.confirm_single_for_queue():
|
||||
return
|
||||
order_id = self.query_order_wait_time()
|
||||
if order_id: # 发送通知
|
||||
self.order_id = order_id
|
||||
self.order_did_success()
|
||||
return True
|
||||
return False
|
||||
|
||||
def order_did_success(self):
|
||||
OrderLog.print_ticket_did_ordered(self.order_id)
|
||||
OrderLog.notification(OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_CONTENT.format(self.user_ins.user_name))
|
||||
self.send_notification()
|
||||
return True
|
||||
|
||||
def send_notification(self):
|
||||
# num = 0 # 通知次数
|
||||
# sustain_time = self.notification_sustain_time
|
||||
info_message = OrderLog.get_order_success_notification_info(self.query_ins)
|
||||
normal_message = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_EMAIL_CONTENT.format(self.order_id, self.user_ins.user_name)
|
||||
if Config().EMAIL_ENABLED: # 邮件通知
|
||||
Notification.send_email(Config().EMAIL_RECEIVER, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
normal_message + info_message)
|
||||
if Config().DINGTALK_ENABLED: # 钉钉通知
|
||||
Notification.dingtalk_webhook(normal_message + info_message)
|
||||
if Config().TELEGRAM_ENABLED: # Telegram推送
|
||||
Notification.send_to_telegram(normal_message + info_message)
|
||||
if Config().SERVERCHAN_ENABLED: # ServerChan通知
|
||||
Notification.server_chan(Config().SERVERCHAN_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
normal_message + info_message)
|
||||
if Config().PUSHBEAR_ENABLED: # PushBear通知
|
||||
Notification.push_bear(Config().PUSHBEAR_KEY, OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_TITLE,
|
||||
normal_message + info_message)
|
||||
if Config().BARK_ENABLED:
|
||||
Notification.push_bark(normal_message+info_message)
|
||||
|
||||
if Config().NOTIFICATION_BY_VOICE_CODE: # 语音通知
|
||||
if Config().NOTIFICATION_VOICE_CODE_TYPE == 'dingxin':
|
||||
voice_info = {
|
||||
'left_station': self.query_ins.left_station,
|
||||
'arrive_station': self.query_ins.arrive_station,
|
||||
'set_type': self.query_ins.current_seat_name,
|
||||
'orderno': self.order_id
|
||||
}
|
||||
else:
|
||||
voice_info = OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_VOICE_CODE_CONTENT.format(
|
||||
self.query_ins.left_station, self.query_ins.arrive_station)
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_ORDER_SUCCESS_NOTIFICATION_OF_VOICE_CODE_START_SEND)
|
||||
Notification.voice_code(Config().NOTIFICATION_VOICE_CODE_PHONE, self.user_ins.get_name(), voice_info)
|
||||
# 取消循环发送通知
|
||||
# while sustain_time: # TODO 后面直接查询有没有待支付的订单就可以
|
||||
# num += 1
|
||||
# else:
|
||||
# break
|
||||
# sustain_time -= self.notification_interval
|
||||
# sleep(self.notification_interval)
|
||||
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_JOB_CLOSED).flush()
|
||||
return True
|
||||
|
||||
def submit_order_request(self):
|
||||
data = {
|
||||
'secretStr': urllib.parse.unquote(self.query_ins.get_info_of_secret_str()), # 解密
|
||||
'train_date': self.query_ins.left_date, # 出发时间
|
||||
'back_train_date': self.query_ins.left_date, # 返程时间
|
||||
'tour_flag': 'dc', # 旅途类型
|
||||
'purpose_codes': 'ADULT', # 成人 | 学生
|
||||
'query_from_station_name': self.query_ins.left_station,
|
||||
'query_to_station_name': self.query_ins.arrive_station,
|
||||
}
|
||||
response = self.session.post(API_SUBMIT_ORDER_REQUEST, data)
|
||||
result = response.json()
|
||||
if result.get('data') == 'N':
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_SUCCESS).flush()
|
||||
return True
|
||||
else:
|
||||
if (str(result.get('messages', '')).find('未处理') >= 0): # 未处理订单
|
||||
# 0125 增加排队时长到 5 分钟之后,更多的是 排队失败,得通过拿到订单列表才能确认,再打个 TODO
|
||||
# self.order_id = 0 # 需要拿到订单号 TODO
|
||||
# return -1
|
||||
pass
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_SUBMIT_ORDER_REQUEST_FAIL.format(
|
||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||
return False
|
||||
|
||||
def check_order_info(self):
|
||||
"""
|
||||
cancel_flag=2
|
||||
bed_level_order_num=000000000000000000000000000000
|
||||
passengerTicketStr=
|
||||
tour_flag=dc
|
||||
randCode=
|
||||
whatsSelect=1
|
||||
_json_att=
|
||||
REPEAT_SUBMIT_TOKEN=458bf1b0a69431f34f9d2e9d3a11cfe9
|
||||
:return:
|
||||
"""
|
||||
data = { #
|
||||
'cancel_flag': 2,
|
||||
'bed_level_order_num': '000000000000000000000000000000',
|
||||
'passengerTicketStr': self.passenger_ticket_str,
|
||||
'oldPassengerStr': self.old_passenger_str,
|
||||
'tour_flag': 'dc',
|
||||
'randCode': '',
|
||||
'whatsSelect': '1',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token
|
||||
}
|
||||
response = self.session.post(API_CHECK_ORDER_INFO, data)
|
||||
result = response.json()
|
||||
if result.get('data.submitStatus'): # 成功
|
||||
# ifShowPassCode 需要验证码
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_CHECK_ORDER_INFO_SUCCESS).flush()
|
||||
if result.get('data.ifShowPassCode') != 'N':
|
||||
self.is_need_auth_code = True
|
||||
|
||||
# if ( ticketInfoForPassengerForm.isAsync == ticket_submit_order.request_flag.isAsync & & ticketInfoForPassengerForm.queryLeftTicketRequestDTO.ypInfoDetail != "") { 不需要排队检测 js TODO
|
||||
return True
|
||||
else:
|
||||
error = CommonLog.MESSAGE_API_RESPONSE_CAN_NOT_BE_HANDLE
|
||||
if not result.get('data.isNoActive'):
|
||||
error = result.get('data.errMsg', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR)
|
||||
else:
|
||||
if result.get('data.checkSeatNum'):
|
||||
error = '无法提交您的订单! ' + result.get('data.errMsg')
|
||||
else:
|
||||
error = '出票失败! ' + result.get('data.errMsg')
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_CHECK_ORDER_INFO_FAIL.format(error)).flush()
|
||||
return False
|
||||
|
||||
def get_queue_count(self):
|
||||
"""
|
||||
获取队列人数
|
||||
train_date Mon Jan 01 2019 00:00:00 GMT+0800 (China Standard Time)
|
||||
train_no 630000Z12208
|
||||
stationTrainCode Z122
|
||||
seatType 4
|
||||
fromStationTelecode GZQ
|
||||
toStationTelecode RXW
|
||||
leftTicket CmDJZYrwUoJ1jFNonIgPzPFdMBvSSE8xfdUwvb2lq8CCWn%2Bzk1vM3roJaHk%3D
|
||||
purpose_codes 00
|
||||
train_location QY
|
||||
_json_att
|
||||
REPEAT_SUBMIT_TOKEN 0977caf26f25d1da43e3213eb35ff87c
|
||||
:return:
|
||||
"""
|
||||
data = { #
|
||||
'train_date': '{} 00:00:00 GMT+0800 (China Standard Time)'.format(
|
||||
datetime.datetime.strptime(self.query_ins.left_date, '%Y-%m-%d').strftime("%a %h %d %Y")),
|
||||
'train_no': self.user_ins.ticket_info_for_passenger_form['queryLeftTicketRequestDTO']['train_no'],
|
||||
'stationTrainCode': self.user_ins.ticket_info_for_passenger_form['queryLeftTicketRequestDTO'][
|
||||
'station_train_code'],
|
||||
'seatType': self.query_ins.current_order_seat,
|
||||
'fromStationTelecode': self.user_ins.ticket_info_for_passenger_form['queryLeftTicketRequestDTO'][
|
||||
'from_station'],
|
||||
'toStationTelecode': self.user_ins.ticket_info_for_passenger_form['queryLeftTicketRequestDTO'][
|
||||
'to_station'],
|
||||
'leftTicket': self.user_ins.ticket_info_for_passenger_form['leftTicketStr'],
|
||||
'purpose_codes': self.user_ins.ticket_info_for_passenger_form['purpose_codes'],
|
||||
'train_location': self.user_ins.ticket_info_for_passenger_form['train_location'],
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token,
|
||||
}
|
||||
response = self.session.post(API_GET_QUEUE_COUNT, data)
|
||||
result = response.json()
|
||||
if result.get('status', False): # 成功
|
||||
"""
|
||||
"data": {
|
||||
"count": "66",
|
||||
"ticket": "0,73",
|
||||
"op_2": "false",
|
||||
"countT": "0",
|
||||
"op_1": "true"
|
||||
}
|
||||
|
||||
"""
|
||||
# if result.get('isRelogin') == 'Y': # 重新登录 TODO
|
||||
|
||||
ticket = result.get('data.ticket').split(',') # 余票列表
|
||||
# 这里可以判断 是真实是 硬座还是无座,避免自动分配到无座
|
||||
ticket_number = ticket[0] # 余票
|
||||
if ticket_number != '充足' and int(ticket_number) <= 0:
|
||||
if self.query_ins.current_seat == SeatType.NO_SEAT: # 允许无座
|
||||
ticket_number = ticket[1]
|
||||
if not int(ticket_number): # 跳过无座
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_INFO_NO_SEAT).flush()
|
||||
return False
|
||||
|
||||
if result.get('data.op_2') == 'true':
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_LESS_TICKET).flush()
|
||||
return False
|
||||
|
||||
current_position = int(result.get('data.countT', 0))
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_GET_QUEUE_INFO_SUCCESS.format(current_position, ticket_number)).flush()
|
||||
return True
|
||||
else:
|
||||
# 加入小黑屋
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_GET_QUEUE_COUNT_FAIL.format(
|
||||
result.get('messages', result.get('validateMessages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR)))).flush()
|
||||
return False
|
||||
|
||||
def confirm_single_for_queue(self):
|
||||
"""
|
||||
确认排队
|
||||
passengerTicketStr
|
||||
oldPassengerStr
|
||||
randCode
|
||||
purpose_codes 00
|
||||
key_check_isChange FEE6C6634A3EAA93E1E6CFC39A99E555A92E438436F18AFF78837CDB
|
||||
leftTicketStr CmDJZYrwUoJ1jFNonIgPzPFdMBvSSE8xfdUwvb2lq8CCWn%2Bzk1vM3roJaHk%3D
|
||||
train_location QY
|
||||
choose_seats
|
||||
seatDetailType 000
|
||||
whatsSelect 1
|
||||
roomType 00
|
||||
dwAll N
|
||||
_json_att
|
||||
REPEAT_SUBMIT_TOKEN 0977caf26f25d1da43e3213eb35ff87c
|
||||
:return:
|
||||
"""
|
||||
data = { #
|
||||
'passengerTicketStr': self.passenger_ticket_str,
|
||||
'oldPassengerStr': self.old_passenger_str,
|
||||
'randCode': '',
|
||||
'purpose_codes': self.user_ins.ticket_info_for_passenger_form['purpose_codes'],
|
||||
'key_check_isChange': self.user_ins.ticket_info_for_passenger_form['key_check_isChange'],
|
||||
'leftTicketStr': self.user_ins.ticket_info_for_passenger_form['leftTicketStr'],
|
||||
'train_location': self.user_ins.ticket_info_for_passenger_form['train_location'],
|
||||
'choose_seats': '',
|
||||
'seatDetailType': '000',
|
||||
'whatsSelect': '1',
|
||||
'roomType': '00',
|
||||
'dwAll': 'N',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token,
|
||||
}
|
||||
|
||||
if self.is_need_auth_code: # 目前好像是都不需要了,有问题再处理
|
||||
pass
|
||||
|
||||
response = self.session.post(API_CONFIRM_SINGLE_FOR_QUEUE, data)
|
||||
result = response.json()
|
||||
|
||||
if 'data' in result:
|
||||
"""
|
||||
"data": {
|
||||
"submitStatus": true
|
||||
}
|
||||
"""
|
||||
if result.get('data.submitStatus'): # 成功
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_SUCCESS).flush()
|
||||
return True
|
||||
else:
|
||||
# 加入小黑屋 TODO
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_ERROR.format(
|
||||
result.get('data.errMsg', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||
else:
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_CONFIRM_SINGLE_FOR_QUEUE_FAIL.format(
|
||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||
return False
|
||||
|
||||
def query_order_wait_time(self):
|
||||
"""
|
||||
排队查询
|
||||
random 1546849953542
|
||||
tourFlag dc
|
||||
_json_att
|
||||
REPEAT_SUBMIT_TOKEN 0977caf26f25d1da43e3213eb35ff87c
|
||||
:return:
|
||||
"""
|
||||
self.current_queue_wait = self.max_queue_wait
|
||||
self.queue_num = 0
|
||||
while self.current_queue_wait:
|
||||
self.current_queue_wait -= self.wait_queue_interval
|
||||
self.queue_num += 1
|
||||
# TODO 取消超时订单,待优化
|
||||
data = { #
|
||||
'random': str(random.random())[2:],
|
||||
'tourFlag': 'dc',
|
||||
'_json_att': '',
|
||||
'REPEAT_SUBMIT_TOKEN': self.user_ins.global_repeat_submit_token,
|
||||
}
|
||||
|
||||
response = self.session.get(API_QUERY_ORDER_WAIT_TIME.format(urllib.parse.urlencode(data)))
|
||||
result = response.json()
|
||||
|
||||
if result.get('status') and 'data' in result:
|
||||
"""
|
||||
"data": {
|
||||
"queryOrderWaitTimeStatus": true,
|
||||
"count": 0,
|
||||
"waitTime": -1,
|
||||
"requestId": 6487958947291482523,
|
||||
"waitCount": 0,
|
||||
"tourFlag": "dc",
|
||||
"orderId": "E222646122"
|
||||
}
|
||||
"""
|
||||
result_data = result['data']
|
||||
order_id = result_data.get('orderId')
|
||||
if order_id: # 成功
|
||||
return order_id
|
||||
elif 'waitTime' in result_data:
|
||||
# 计算等待时间
|
||||
wait_time = int(result_data.get('waitTime'))
|
||||
if wait_time == -1: # 成功
|
||||
# /otn/confirmPassenger/resultOrderForDcQueue 请求订单状态 目前不需要
|
||||
# 不应该走到这
|
||||
return order_id
|
||||
elif wait_time == -100: # 重新获取订单号
|
||||
pass
|
||||
elif wait_time >= 0: # 等待
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_WAITING.format(result_data.get('waitCount', 0),
|
||||
wait_time)).flush()
|
||||
else:
|
||||
if wait_time == -2 or wait_time == -3: # -2 失败 -3 订单已撤销
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_FAIL.format(result_data.get('msg'))).flush()
|
||||
return False
|
||||
else: # 未知原因
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_FAIL.format(
|
||||
result_data.get('msg', wait_time))).flush()
|
||||
return False
|
||||
|
||||
elif result_data.get('msg'): # 失败 对不起,由于您取消次数过多,今日将不能继续受理您的订票请求。1月8日您可继续使用订票功能。
|
||||
# TODO 需要增加判断 直接结束
|
||||
OrderLog.add_quick_log(
|
||||
OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_FAIL.format(
|
||||
result_data.get('msg', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR))).flush()
|
||||
stay_second(self.retry_time)
|
||||
return False
|
||||
elif result.get('messages') or result.get('validateMessages'):
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_FAIL.format(
|
||||
result.get('messages', result.get('validateMessages')))).flush()
|
||||
return False
|
||||
else:
|
||||
pass
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_QUERY_ORDER_WAIT_TIME_INFO.format(self.queue_num)).flush()
|
||||
stay_second(self.wait_queue_interval)
|
||||
|
||||
return False
|
||||
|
||||
def make_passenger_ticket_str(self):
|
||||
"""
|
||||
生成提交车次的内容
|
||||
格式:
|
||||
1(seatType),0,1(车票类型:ticket_type_codes),张三(passenger_name),1(证件类型:passenger_id_type_code),xxxxxx(passenger_id_no),xxxx(mobile_no),N
|
||||
passengerTicketStr:
|
||||
张三(passenger_name),1(证件类型:passenger_id_type_code),xxxxxx(passenger_id_no),1_
|
||||
oldPassengerStr
|
||||
:return:
|
||||
"""
|
||||
passenger_tickets = []
|
||||
old_passengers = []
|
||||
available_passengers = self.query_ins.passengers
|
||||
if len(available_passengers) > self.query_ins.member_num_take: # 删除人数
|
||||
available_passengers = available_passengers[0:self.query_ins.member_num_take]
|
||||
OrderLog.print_passenger_did_deleted(available_passengers)
|
||||
|
||||
for passenger in available_passengers:
|
||||
tmp_str = '{seat_type},0,{passenger_type},{passenger_name},{passenger_id_card_type},{passenger_id_card},{passenger_mobile},N,{enc_str}_'.format(
|
||||
seat_type=self.query_ins.current_order_seat, passenger_type=passenger['type'],
|
||||
passenger_name=passenger['name'],
|
||||
passenger_id_card_type=passenger['id_card_type'], passenger_id_card=passenger['id_card'],
|
||||
passenger_mobile=passenger['mobile'],
|
||||
enc_str=passenger['enc_str'],
|
||||
)
|
||||
passenger_tickets.append(tmp_str)
|
||||
|
||||
if int(passenger['type']) != UserType.CHILD:
|
||||
tmp_old_str = '{passenger_name},{passenger_id_card_type},{passenger_id_card},{passenger_type}_'.format(
|
||||
passenger_name=passenger['name'],
|
||||
passenger_id_card_type=passenger['id_card_type'], passenger_id_card=passenger['id_card'],
|
||||
passenger_type=passenger['type'],
|
||||
)
|
||||
old_passengers.append(tmp_old_str)
|
||||
|
||||
self.passenger_ticket_str = ''.join(passenger_tickets).rstrip('_')
|
||||
self.old_passenger_str = ''.join(old_passengers).rstrip('_') + '__ _ _' # 不加后面请求会出错
|
||||
@@ -1,377 +0,0 @@
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from py12306.app import app_available_check
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.api import LEFT_TICKETS
|
||||
from py12306.helpers.station import Station
|
||||
from py12306.helpers.type import OrderSeatType, SeatType
|
||||
from py12306.log.query_log import QueryLog
|
||||
from py12306.helpers.func import *
|
||||
from py12306.log.user_log import UserLog
|
||||
from py12306.order.order import Order
|
||||
from py12306.user.user import User
|
||||
from py12306.helpers.event import Event
|
||||
|
||||
|
||||
class Job:
|
||||
"""
|
||||
查询任务
|
||||
"""
|
||||
id = 0
|
||||
is_alive = True
|
||||
job_name = None
|
||||
left_dates = []
|
||||
left_date = None
|
||||
stations = []
|
||||
left_station = ''
|
||||
arrive_station = ''
|
||||
left_station_code = ''
|
||||
arrive_station_code = ''
|
||||
from_time = timedelta(hours=0)
|
||||
to_time = timedelta(hours=24)
|
||||
|
||||
account_key = 0
|
||||
allow_seats = []
|
||||
current_seat = None
|
||||
current_seat_name = ''
|
||||
current_order_seat = None
|
||||
allow_train_numbers = []
|
||||
except_train_numbers = []
|
||||
members = []
|
||||
member_num = 0
|
||||
member_num_take = 0 # 最终提交的人数
|
||||
passengers = []
|
||||
allow_less_member = False
|
||||
retry_time = 3
|
||||
|
||||
interval = {}
|
||||
interval_additional = 0
|
||||
interval_additional_max = 5
|
||||
|
||||
query = None
|
||||
cluster = None
|
||||
ticket_info = {}
|
||||
is_cdn = False
|
||||
query_time_out = 3
|
||||
INDEX_TICKET_NUM = 11
|
||||
INDEX_TRAIN_NUMBER = 3
|
||||
INDEX_TRAIN_NO = 2
|
||||
INDEX_LEFT_DATE = 13
|
||||
INDEX_LEFT_STATION = 6 # 4 5 始发 终点
|
||||
INDEX_ARRIVE_STATION = 7
|
||||
INDEX_ORDER_TEXT = 1 # 下单文字
|
||||
INDEX_SECRET_STR = 0
|
||||
INDEX_LEFT_TIME = 8
|
||||
INDEX_ARRIVE_TIME = 9
|
||||
|
||||
max_buy_time = 32
|
||||
|
||||
def __init__(self, info, query):
|
||||
self.cluster = Cluster()
|
||||
self.query = query
|
||||
self.init_data(info)
|
||||
self.update_interval()
|
||||
|
||||
def init_data(self, info):
|
||||
self.id = md5(info)
|
||||
self.left_dates = info.get('left_dates')
|
||||
self.stations = info.get('stations')
|
||||
self.stations = [self.stations] if isinstance(self.stations, dict) else self.stations
|
||||
if not self.job_name: # name 不能被修改
|
||||
self.job_name = info.get('job_name',
|
||||
'{} -> {}'.format(self.stations[0]['left'], self.stations[0]['arrive']))
|
||||
|
||||
self.account_key = str(info.get('account_key'))
|
||||
self.allow_seats = info.get('seats')
|
||||
self.allow_train_numbers = info.get('train_numbers')
|
||||
self.except_train_numbers = info.get('except_train_numbers')
|
||||
self.members = list(map(str, info.get('members')))
|
||||
self.member_num = len(self.members)
|
||||
self.member_num_take = self.member_num
|
||||
self.allow_less_member = bool(info.get('allow_less_member'))
|
||||
period = info.get('period')
|
||||
if isinstance(period, dict):
|
||||
if 'from' in period:
|
||||
parts = period['from'].split(':')
|
||||
if len(parts) == 2:
|
||||
self.from_time = timedelta(
|
||||
hours=int(parts[0]), seconds=int(parts[1]))
|
||||
if 'to' in period:
|
||||
parts = period['to'].split(':')
|
||||
if len(parts) == 2:
|
||||
self.to_time = timedelta(
|
||||
hours=int(parts[0]), seconds=int(parts[1]))
|
||||
|
||||
def update_interval(self):
|
||||
self.interval = self.query.interval
|
||||
|
||||
def run(self):
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
处理单个任务
|
||||
根据日期循环查询, 展示处理时间
|
||||
:param job:
|
||||
:return:
|
||||
"""
|
||||
while True and self.is_alive:
|
||||
app_available_check()
|
||||
QueryLog.print_job_start(self.job_name)
|
||||
for station in self.stations:
|
||||
self.refresh_station(station)
|
||||
for date in self.left_dates:
|
||||
self.left_date = date
|
||||
response = self.query_by_date(date)
|
||||
self.handle_response(response)
|
||||
QueryLog.add_query_time_log(time=response.elapsed.total_seconds(), is_cdn=self.is_cdn)
|
||||
if not self.is_alive: return
|
||||
self.safe_stay()
|
||||
if is_main_thread():
|
||||
QueryLog.flush(sep='\t\t', publish=False)
|
||||
if not Config().QUERY_JOB_THREAD_ENABLED:
|
||||
QueryLog.add_quick_log('').flush(publish=False)
|
||||
break
|
||||
else:
|
||||
QueryLog.add_log('\n').flush(sep='\t\t', publish=False)
|
||||
if Const.IS_TEST: return
|
||||
|
||||
def judge_date_legal(self, date):
|
||||
date_now = datetime.datetime.now()
|
||||
date_query = datetime.datetime.strptime(str(date), "%Y-%m-%d")
|
||||
diff = (date_query - date_now).days
|
||||
if date_now.day == date_query.day:
|
||||
diff = 0
|
||||
if diff < 0:
|
||||
msg = '乘车日期错误,比当前时间还早!!'
|
||||
QueryLog.add_quick_log(msg).flush(publish=False)
|
||||
raise RuntimeError(msg)
|
||||
elif diff > self.max_buy_time:
|
||||
msg = '乘车日期错误,超出一个月预售期!!'
|
||||
QueryLog.add_quick_log(msg).flush(publish=False)
|
||||
raise RuntimeError(msg)
|
||||
else:
|
||||
pass
|
||||
|
||||
def query_by_date(self, date):
|
||||
"""
|
||||
通过日期进行查询
|
||||
:return:
|
||||
"""
|
||||
self.judge_date_legal(date)
|
||||
from py12306.helpers.cdn import Cdn
|
||||
QueryLog.add_log(('\n' if not is_main_thread() else '') + QueryLog.MESSAGE_QUERY_START_BY_DATE.format(date,
|
||||
self.left_station,
|
||||
self.arrive_station))
|
||||
url = LEFT_TICKETS.get('url').format(left_date=date, left_station=self.left_station_code,
|
||||
arrive_station=self.arrive_station_code, type=self.query.api_type)
|
||||
if Config.is_cdn_enabled() and Cdn().is_ready:
|
||||
self.is_cdn = True
|
||||
return self.query.session.cdn_request(url, timeout=self.query_time_out, allow_redirects=False)
|
||||
self.is_cdn = False
|
||||
return self.query.session.get(url, timeout=self.query_time_out, allow_redirects=False)
|
||||
|
||||
def handle_response(self, response):
|
||||
"""
|
||||
错误判断
|
||||
余票判断
|
||||
小黑屋判断
|
||||
座位判断
|
||||
乘车人判断
|
||||
:param result:
|
||||
:return:
|
||||
"""
|
||||
results = self.get_results(response)
|
||||
if not results:
|
||||
return False
|
||||
for result in results:
|
||||
self.ticket_info = ticket_info = result.split('|')
|
||||
if not self.is_trains_number_valid(): # 车次是否有效
|
||||
continue
|
||||
QueryLog.add_log(QueryLog.MESSAGE_QUERY_LOG_OF_EVERY_TRAIN.format(self.get_info_of_train_number()))
|
||||
if not self.is_has_ticket(ticket_info):
|
||||
continue
|
||||
allow_seats = self.allow_seats if self.allow_seats else list(
|
||||
Config.SEAT_TYPES.values()) # 未设置 则所有可用 TODO 合法检测
|
||||
self.handle_seats(allow_seats, ticket_info)
|
||||
if not self.is_alive: return
|
||||
|
||||
def handle_seats(self, allow_seats, ticket_info):
|
||||
for seat in allow_seats: # 检查座位是否有票
|
||||
self.set_seat(seat)
|
||||
ticket_of_seat = ticket_info[self.current_seat]
|
||||
if not self.is_has_ticket_by_seat(ticket_of_seat): # 座位是否有效
|
||||
continue
|
||||
QueryLog.print_ticket_seat_available(left_date=self.get_info_of_left_date(),
|
||||
train_number=self.get_info_of_train_number(), seat_type=seat,
|
||||
rest_num=ticket_of_seat)
|
||||
if not self.is_member_number_valid(ticket_of_seat): # 乘车人数是否有效
|
||||
if self.allow_less_member:
|
||||
self.member_num_take = int(ticket_of_seat)
|
||||
QueryLog.print_ticket_num_less_than_specified(ticket_of_seat, self)
|
||||
else:
|
||||
QueryLog.add_quick_log(
|
||||
QueryLog.MESSAGE_GIVE_UP_CHANCE_CAUSE_TICKET_NUM_LESS_THAN_SPECIFIED).flush()
|
||||
continue
|
||||
if Const.IS_TEST: return
|
||||
# 检查完成 开始提交订单
|
||||
QueryLog.print_ticket_available(left_date=self.get_info_of_left_date(),
|
||||
train_number=self.get_info_of_train_number(),
|
||||
rest_num=ticket_of_seat)
|
||||
if User.is_empty():
|
||||
QueryLog.add_quick_log(QueryLog.MESSAGE_USER_IS_EMPTY_WHEN_DO_ORDER.format(self.retry_time))
|
||||
return stay_second(self.retry_time)
|
||||
|
||||
order_result = False
|
||||
user = self.get_user()
|
||||
if not user:
|
||||
QueryLog.add_quick_log(QueryLog.MESSAGE_ORDER_USER_IS_EMPTY.format(self.retry_time))
|
||||
return stay_second(self.retry_time)
|
||||
|
||||
lock_id = Cluster.KEY_LOCK_DO_ORDER + '_' + user.key
|
||||
if Config().is_cluster_enabled():
|
||||
if self.cluster.get_lock(lock_id, Cluster.lock_do_order_time,
|
||||
{'node': self.cluster.node_name}): # 获得下单锁
|
||||
order_result = self.do_order(user)
|
||||
if not order_result: # 下单失败,解锁
|
||||
self.cluster.release_lock(lock_id)
|
||||
else:
|
||||
QueryLog.add_quick_log(
|
||||
QueryLog.MESSAGE_SKIP_ORDER.format(self.cluster.get_lock_info(lock_id).get('node'),
|
||||
user.user_name))
|
||||
stay_second(self.retry_time) # 防止过多重复
|
||||
else:
|
||||
order_result = self.do_order(user)
|
||||
|
||||
# 任务已成功 通知集群停止任务
|
||||
if order_result:
|
||||
Event().job_destroy({'name': self.job_name})
|
||||
|
||||
def do_order(self, user):
|
||||
self.check_passengers()
|
||||
order = Order(user=user, query=self)
|
||||
return order.order()
|
||||
|
||||
def get_results(self, response):
|
||||
"""
|
||||
解析查询返回结果
|
||||
:param response:
|
||||
:return:
|
||||
"""
|
||||
if response.status_code != 200:
|
||||
QueryLog.print_query_error(response.reason, response.status_code)
|
||||
if self.interval_additional < self.interval_additional_max:
|
||||
self.interval_additional += self.interval.get('min')
|
||||
else:
|
||||
self.interval_additional = 0
|
||||
result = response.json().get('data.result')
|
||||
return result if result else False
|
||||
|
||||
def is_has_ticket(self, ticket_info):
|
||||
return self.get_info_of_ticket_num() == 'Y' and self.get_info_of_order_text() == '预订'
|
||||
|
||||
def is_has_ticket_by_seat(self, seat):
|
||||
return seat != '' and seat != '无' and seat != '*'
|
||||
|
||||
def is_trains_number_valid(self):
|
||||
train_left_time = self.get_info_of_train_left_time()
|
||||
time_parts = train_left_time.split(':')
|
||||
left_time = timedelta(
|
||||
hours=int(time_parts[0]), seconds=int(time_parts[1]))
|
||||
if left_time < self.from_time or left_time > self.to_time:
|
||||
return False
|
||||
|
||||
if self.except_train_numbers:
|
||||
return self.get_info_of_train_number().upper() not in map(str.upper, self.except_train_numbers)
|
||||
if self.allow_train_numbers:
|
||||
return self.get_info_of_train_number().upper() in map(str.upper, self.allow_train_numbers)
|
||||
return True
|
||||
|
||||
def is_member_number_valid(self, seat):
|
||||
return seat == '有' or self.member_num <= int(seat)
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
退出任务
|
||||
:return:
|
||||
"""
|
||||
from py12306.query.query import Query
|
||||
self.is_alive = False
|
||||
QueryLog.add_quick_log(QueryLog.MESSAGE_QUERY_JOB_BEING_DESTROY.format(self.job_name)).flush()
|
||||
# sys.exit(1) # 无法退出线程...
|
||||
# 手动移出jobs 防止单线程死循环
|
||||
index = Query().jobs.index(self)
|
||||
Query().jobs.pop(index)
|
||||
|
||||
def safe_stay(self):
|
||||
origin_interval = get_interval_num(self.interval)
|
||||
interval = origin_interval + self.interval_additional
|
||||
QueryLog.add_stay_log(
|
||||
'%s + %s' % (origin_interval, self.interval_additional) if self.interval_additional else origin_interval)
|
||||
stay_second(interval)
|
||||
|
||||
def set_passengers(self, passengers):
|
||||
UserLog.print_user_passenger_init_success(passengers)
|
||||
self.passengers = passengers
|
||||
|
||||
def set_seat(self, seat):
|
||||
self.current_seat_name = seat
|
||||
self.current_seat = SeatType.dicts.get(seat)
|
||||
self.current_order_seat = OrderSeatType.dicts.get(seat)
|
||||
|
||||
def get_user(self):
|
||||
user = User.get_user(self.account_key)
|
||||
# if not user.check_is_ready(): # 这里不需要检测了,后面获取乘客时已经检测过
|
||||
# #
|
||||
# pass
|
||||
return user
|
||||
|
||||
def check_passengers(self):
|
||||
if not self.passengers:
|
||||
QueryLog.add_quick_log(QueryLog.MESSAGE_CHECK_PASSENGERS.format(self.job_name)).flush()
|
||||
passengers = User.get_passenger_for_members(self.members, self.account_key)
|
||||
if passengers:
|
||||
self.set_passengers(passengers)
|
||||
else: # 退出当前查询任务
|
||||
self.destroy()
|
||||
return True
|
||||
|
||||
def refresh_station(self, station):
|
||||
self.left_station = station.get('left')
|
||||
self.arrive_station = station.get('arrive')
|
||||
self.left_station_code = Station.get_station_key_by_name(self.left_station)
|
||||
self.arrive_station_code = Station.get_station_key_by_name(self.arrive_station)
|
||||
|
||||
# 提供一些便利方法
|
||||
def get_info_of_left_date(self):
|
||||
return self.ticket_info[self.INDEX_LEFT_DATE]
|
||||
|
||||
def get_info_of_ticket_num(self):
|
||||
return self.ticket_info[self.INDEX_TICKET_NUM]
|
||||
|
||||
def get_info_of_train_number(self):
|
||||
return self.ticket_info[self.INDEX_TRAIN_NUMBER]
|
||||
|
||||
def get_info_of_train_no(self):
|
||||
return self.ticket_info[self.INDEX_TRAIN_NO]
|
||||
|
||||
def get_info_of_left_station(self):
|
||||
return Station.get_station_name_by_key(self.ticket_info[self.INDEX_LEFT_STATION])
|
||||
|
||||
def get_info_of_arrive_station(self):
|
||||
return Station.get_station_name_by_key(self.ticket_info[self.INDEX_ARRIVE_STATION])
|
||||
|
||||
def get_info_of_order_text(self):
|
||||
return self.ticket_info[self.INDEX_ORDER_TEXT]
|
||||
|
||||
def get_info_of_secret_str(self):
|
||||
return self.ticket_info[self.INDEX_SECRET_STR]
|
||||
|
||||
def get_info_of_train_left_time(self):
|
||||
return self.ticket_info[self.INDEX_LEFT_TIME]
|
||||
|
||||
def get_info_of_train_arrive_time(self):
|
||||
return self.ticket_info[self.INDEX_ARRIVE_TIME]
|
||||
@@ -1,174 +0,0 @@
|
||||
from py12306.config import Config
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.app import app_available_check
|
||||
from py12306.helpers.func import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.log.query_log import QueryLog
|
||||
from py12306.query.job import Job
|
||||
from py12306.helpers.api import API_QUERY_INIT_PAGE
|
||||
|
||||
|
||||
@singleton
|
||||
class Query:
|
||||
"""
|
||||
余票查询
|
||||
|
||||
"""
|
||||
jobs = []
|
||||
query_jobs = []
|
||||
session = {}
|
||||
|
||||
# 查询间隔
|
||||
interval = {}
|
||||
cluster = None
|
||||
|
||||
is_in_thread = False
|
||||
retry_time = 3
|
||||
is_ready = False
|
||||
api_type = None # Query api url, Current know value leftTicket/queryX | leftTicket/queryZ
|
||||
|
||||
def __init__(self):
|
||||
self.session = Request()
|
||||
self.cluster = Cluster()
|
||||
self.update_query_interval()
|
||||
self.update_query_jobs()
|
||||
self.get_query_api_type()
|
||||
|
||||
def update_query_interval(self, auto=False):
|
||||
self.interval = init_interval_by_number(Config().QUERY_INTERVAL)
|
||||
if auto:
|
||||
jobs_do(self.jobs, 'update_interval')
|
||||
|
||||
def update_query_jobs(self, auto=False):
|
||||
self.query_jobs = Config().QUERY_JOBS
|
||||
if auto:
|
||||
QueryLog.add_quick_log(QueryLog.MESSAGE_JOBS_DID_CHANGED).flush()
|
||||
self.refresh_jobs()
|
||||
if not Config().is_slave():
|
||||
jobs_do(self.jobs, 'check_passengers')
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
app_available_check()
|
||||
self.start()
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def check_before_run(cls):
|
||||
self = cls()
|
||||
self.init_jobs()
|
||||
self.is_ready = True
|
||||
|
||||
def start(self):
|
||||
# return # DEBUG
|
||||
QueryLog.init_data()
|
||||
stay_second(3)
|
||||
# 多线程
|
||||
while True:
|
||||
if Config().QUERY_JOB_THREAD_ENABLED: # 多线程
|
||||
if not self.is_in_thread:
|
||||
self.is_in_thread = True
|
||||
create_thread_and_run(jobs=self.jobs, callback_name='run', wait=Const.IS_TEST)
|
||||
if Const.IS_TEST: return
|
||||
stay_second(self.retry_time)
|
||||
else:
|
||||
if not self.jobs: break
|
||||
self.is_in_thread = False
|
||||
jobs_do(self.jobs, 'run')
|
||||
if Const.IS_TEST: return
|
||||
|
||||
# while True:
|
||||
# app_available_check()
|
||||
# if Config().QUERY_JOB_THREAD_ENABLED: # 多线程
|
||||
# create_thread_and_run(jobs=self.jobs, callback_name='run')
|
||||
# else:
|
||||
# for job in self.jobs: job.run()
|
||||
# if Const.IS_TEST: return
|
||||
# self.refresh_jobs() # 刷新任务
|
||||
|
||||
def refresh_jobs(self):
|
||||
"""
|
||||
更新任务
|
||||
:return:
|
||||
"""
|
||||
allow_jobs = []
|
||||
for job in self.query_jobs:
|
||||
id = md5(job)
|
||||
job_ins = objects_find_object_by_key_value(self.jobs, 'id', id) # [1 ,2]
|
||||
if not job_ins:
|
||||
job_ins = self.init_job(job)
|
||||
if Config().QUERY_JOB_THREAD_ENABLED: # 多线程重新添加
|
||||
create_thread_and_run(jobs=job_ins, callback_name='run', wait=Const.IS_TEST)
|
||||
allow_jobs.append(job_ins)
|
||||
|
||||
for job in self.jobs: # 退出已删除 Job
|
||||
if job not in allow_jobs: job.destroy()
|
||||
|
||||
QueryLog.print_init_jobs(jobs=self.jobs)
|
||||
|
||||
def init_jobs(self):
|
||||
for job in self.query_jobs:
|
||||
self.init_job(job)
|
||||
QueryLog.print_init_jobs(jobs=self.jobs)
|
||||
|
||||
def init_job(self, job):
|
||||
job = Job(info=job, query=self)
|
||||
self.jobs.append(job)
|
||||
return job
|
||||
|
||||
@classmethod
|
||||
def wait_for_ready(cls):
|
||||
self = cls()
|
||||
if self.is_ready: return self
|
||||
stay_second(self.retry_time)
|
||||
return self.wait_for_ready()
|
||||
|
||||
@classmethod
|
||||
def job_by_name(cls, name) -> Job:
|
||||
self = cls()
|
||||
for job in self.jobs:
|
||||
if job.job_name == name: return job
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def job_by_name(cls, name) -> Job:
|
||||
self = cls()
|
||||
return objects_find_object_by_key_value(self.jobs, 'job_name', name)
|
||||
|
||||
@classmethod
|
||||
def job_by_account_key(cls, account_key) -> Job:
|
||||
self = cls()
|
||||
return objects_find_object_by_key_value(self.jobs, 'account_key', account_key)
|
||||
|
||||
@classmethod
|
||||
def get_query_api_type(cls):
|
||||
import re
|
||||
self = cls()
|
||||
if self.api_type:
|
||||
return self.api_type
|
||||
response = self.session.get(API_QUERY_INIT_PAGE)
|
||||
if response.status_code == 200:
|
||||
res = re.search(r'var CLeftTicketUrl = \'(.*)\';', response.text)
|
||||
try:
|
||||
self.api_type = res.group(1)
|
||||
except IndexError:
|
||||
pass
|
||||
return cls.get_query_api_type()
|
||||
|
||||
# def get_jobs_from_cluster(self):
|
||||
# jobs = self.cluster.session.get_dict(Cluster.KEY_JOBS)
|
||||
# return jobs
|
||||
#
|
||||
# def update_jobs_of_cluster(self):
|
||||
# if config.CLUSTER_ENABLED and config.NODE_IS_MASTER:
|
||||
# return self.cluster.session.set_dict(Cluster.KEY_JOBS, self.query_jobs)
|
||||
#
|
||||
# def refresh_jobs(self):
|
||||
# if not config.CLUSTER_ENABLED: return
|
||||
# jobs = self.get_jobs_from_cluster()
|
||||
# if jobs != self.query_jobs:
|
||||
# self.jobs = []
|
||||
# self.query_jobs = jobs
|
||||
# QueryLog.add_quick_log(QueryLog.MESSAGE_JOBS_DID_CHANGED).flush()
|
||||
# self.init_jobs()
|
||||
@@ -1,422 +0,0 @@
|
||||
import json
|
||||
import pickle
|
||||
import re
|
||||
from os import path
|
||||
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.helpers.api import *
|
||||
from py12306.app import *
|
||||
from py12306.helpers.auth_code import AuthCode
|
||||
from py12306.helpers.event import Event
|
||||
from py12306.helpers.func import *
|
||||
from py12306.helpers.request import Request
|
||||
from py12306.helpers.type import UserType
|
||||
from py12306.log.order_log import OrderLog
|
||||
from py12306.log.user_log import UserLog
|
||||
from py12306.log.common_log import CommonLog
|
||||
|
||||
|
||||
class UserJob:
|
||||
# heartbeat = 60 * 2 # 心跳保持时长
|
||||
is_alive = True
|
||||
check_interval = 5
|
||||
key = None
|
||||
user_name = ''
|
||||
password = ''
|
||||
user = None
|
||||
info = {} # 用户信息
|
||||
last_heartbeat = None
|
||||
is_ready = False
|
||||
user_loaded = False # 用户是否已加载成功
|
||||
passengers = []
|
||||
retry_time = 3
|
||||
login_num = 0 # 尝试登录次数
|
||||
|
||||
# Init page
|
||||
global_repeat_submit_token = None
|
||||
ticket_info_for_passenger_form = None
|
||||
order_request_dto = None
|
||||
|
||||
cluster = None
|
||||
lock_init_user_time = 3 * 60
|
||||
cookie = False
|
||||
|
||||
def __init__(self, info):
|
||||
self.cluster = Cluster()
|
||||
self.init_data(info)
|
||||
|
||||
def init_data(self, info):
|
||||
self.session = Request()
|
||||
self.session.add_response_hook(self.response_login_check)
|
||||
self.key = str(info.get('key'))
|
||||
self.user_name = info.get('user_name')
|
||||
self.password = info.get('password')
|
||||
|
||||
def update_user(self):
|
||||
from py12306.user.user import User
|
||||
self.user = User()
|
||||
self.load_user()
|
||||
|
||||
def run(self):
|
||||
# load user
|
||||
self.update_user()
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
检测心跳
|
||||
:return:
|
||||
"""
|
||||
while True and self.is_alive:
|
||||
app_available_check()
|
||||
if Config().is_slave():
|
||||
self.load_user_from_remote()
|
||||
else:
|
||||
if Config().is_master() and not self.cookie: self.load_user_from_remote() # 主节点加载一次 Cookie
|
||||
self.check_heartbeat()
|
||||
if Const.IS_TEST: return
|
||||
stay_second(self.check_interval)
|
||||
|
||||
def check_heartbeat(self):
|
||||
# 心跳检测
|
||||
if self.get_last_heartbeat() and (time_int() - self.get_last_heartbeat()) < Config().USER_HEARTBEAT_INTERVAL:
|
||||
return True
|
||||
# 只有主节点才能走到这
|
||||
if self.is_first_time() or not self.check_user_is_login():
|
||||
if not self.handle_login(): return
|
||||
|
||||
self.user_did_load()
|
||||
message = UserLog.MESSAGE_USER_HEARTBEAT_NORMAL.format(self.get_name(), Config().USER_HEARTBEAT_INTERVAL)
|
||||
UserLog.add_quick_log(message).flush()
|
||||
|
||||
def get_last_heartbeat(self):
|
||||
if Config().is_cluster_enabled():
|
||||
return int(self.cluster.session.get(Cluster.KEY_USER_LAST_HEARTBEAT, 0))
|
||||
|
||||
return self.last_heartbeat
|
||||
|
||||
def set_last_heartbeat(self, time=None):
|
||||
time = time if time != None else time_int()
|
||||
if Config().is_cluster_enabled():
|
||||
self.cluster.session.set(Cluster.KEY_USER_LAST_HEARTBEAT, time)
|
||||
self.last_heartbeat = time
|
||||
|
||||
# def init_cookies
|
||||
def is_first_time(self):
|
||||
if Config().is_cluster_enabled():
|
||||
return not self.cluster.get_user_cookie(self.key)
|
||||
return not path.exists(self.get_cookie_path())
|
||||
|
||||
def handle_login(self, expire=False):
|
||||
if expire: UserLog.print_user_expired()
|
||||
self.is_ready = False
|
||||
UserLog.print_start_login(user=self)
|
||||
return self.login()
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
获取验证码结果
|
||||
:return 权限校验码
|
||||
"""
|
||||
data = {
|
||||
'username': self.user_name,
|
||||
'password': self.password,
|
||||
'appid': 'otn'
|
||||
}
|
||||
answer = AuthCode.get_auth_code(self.session)
|
||||
data['answer'] = answer
|
||||
self.request_device_id()
|
||||
response = self.session.post(API_BASE_LOGIN.get('url'), data)
|
||||
result = response.json()
|
||||
if result.get('result_code') == 0: # 登录成功
|
||||
"""
|
||||
login 获得 cookie uamtk
|
||||
auth/uamtk 不请求,会返回 uamtk票据内容为空
|
||||
/otn/uamauthclient 能拿到用户名
|
||||
"""
|
||||
new_tk = self.auth_uamtk()
|
||||
user_name = self.auth_uamauthclient(new_tk)
|
||||
self.update_user_info({'user_name': user_name})
|
||||
self.login_did_success()
|
||||
return True
|
||||
elif result.get('result_code') == 2: # 账号之内错误
|
||||
# 登录失败,用户名或密码为空
|
||||
# 密码输入错误
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message'))).flush()
|
||||
else:
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_LOGIN_FAIL.format(result.get('result_message', result.get('message',
|
||||
CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR)))).flush()
|
||||
|
||||
return False
|
||||
|
||||
def check_user_is_login(self):
|
||||
response = self.session.get(API_USER_LOGIN_CHECK)
|
||||
is_login = response.json().get('data.is_login', False) == 'Y'
|
||||
if is_login:
|
||||
self.save_user()
|
||||
self.set_last_heartbeat()
|
||||
return self.get_user_info() # 检测应该是不会维持状态,这里再请求下个人中心看有没有用,01-10 看来应该是没用 01-22 有时拿到的状态 是已失效的再加上试试
|
||||
|
||||
return is_login
|
||||
|
||||
def auth_uamtk(self):
|
||||
response = self.session.post(API_AUTH_UAMTK.get('url'), {'appid': 'otn'}, headers={
|
||||
'Referer': 'https://kyfw.12306.cn/otn/passport?redirect=/otn/login/userLogin',
|
||||
'Origin': 'https://kyfw.12306.cn'
|
||||
})
|
||||
result = response.json()
|
||||
if result.get('newapptk'):
|
||||
return result.get('newapptk')
|
||||
# TODO 处理获取失败情况
|
||||
return False
|
||||
|
||||
def auth_uamauthclient(self, tk):
|
||||
response = self.session.post(API_AUTH_UAMAUTHCLIENT.get('url'), {'tk': tk})
|
||||
result = response.json()
|
||||
if result.get('username'):
|
||||
return result.get('username')
|
||||
# TODO 处理获取失败情况
|
||||
return False
|
||||
|
||||
def request_device_id(self):
|
||||
"""
|
||||
获取加密后的浏览器特征 ID
|
||||
:return:
|
||||
"""
|
||||
response = self.session.get(API_GET_BROWSER_DEVICE_ID)
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
result = json.loads(response.text)
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
|
||||
}
|
||||
from base64 import b64decode
|
||||
self.session.headers.update(headers)
|
||||
response = self.session.get(b64decode(result['id']).decode())
|
||||
if response.text.find('callbackFunction') >= 0:
|
||||
result = response.text[18:-2]
|
||||
result = json.loads(result)
|
||||
self.session.cookies.update({
|
||||
'RAIL_EXPIRATION': result.get('exp'),
|
||||
'RAIL_DEVICEID': result.get('dfp'),
|
||||
})
|
||||
except:
|
||||
return False
|
||||
|
||||
def login_did_success(self):
|
||||
"""
|
||||
用户登录成功
|
||||
:return:
|
||||
"""
|
||||
self.login_num += 1
|
||||
self.welcome_user()
|
||||
self.save_user()
|
||||
self.get_user_info()
|
||||
self.set_last_heartbeat()
|
||||
self.is_ready = True
|
||||
|
||||
def welcome_user(self):
|
||||
UserLog.print_welcome_user(self)
|
||||
pass
|
||||
|
||||
def get_cookie_path(self):
|
||||
return Config().USER_DATA_DIR + self.user_name + '.cookie'
|
||||
|
||||
def update_user_info(self, info):
|
||||
self.info = {**self.info, **info}
|
||||
|
||||
def get_name(self):
|
||||
return self.info.get('user_name', '')
|
||||
|
||||
def save_user(self):
|
||||
if Config().is_master():
|
||||
self.cluster.set_user_cookie(self.key, self.session.cookies)
|
||||
self.cluster.set_user_info(self.key, self.info)
|
||||
with open(self.get_cookie_path(), 'wb') as f:
|
||||
pickle.dump(self.session.cookies, f)
|
||||
|
||||
def did_loaded_user(self):
|
||||
"""
|
||||
恢复用户成功
|
||||
:return:
|
||||
"""
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER.format(self.user_name)).flush()
|
||||
if self.check_user_is_login() and self.get_user_info():
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_SUCCESS.format(self.user_name)).flush()
|
||||
UserLog.print_welcome_user(self)
|
||||
self.user_did_load()
|
||||
else:
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_LOADED_USER_BUT_EXPIRED).flush()
|
||||
self.set_last_heartbeat(0)
|
||||
|
||||
def user_did_load(self):
|
||||
"""
|
||||
用户已经加载成功
|
||||
:return:
|
||||
"""
|
||||
self.is_ready = True
|
||||
if self.user_loaded: return
|
||||
self.user_loaded = True
|
||||
Event().user_loaded({'key': self.key}) # 发布通知
|
||||
|
||||
def get_user_info(self):
|
||||
response = self.session.get(API_USER_INFO.get('url'))
|
||||
result = response.json()
|
||||
user_data = result.get('data.userDTO.loginUserDTO')
|
||||
# 子节点访问会导致主节点登录失效 TODO 可快考虑实时同步 cookie
|
||||
if user_data:
|
||||
self.update_user_info({**user_data, **{'user_name': user_data.get('name')}})
|
||||
self.save_user()
|
||||
return True
|
||||
return False
|
||||
|
||||
def load_user(self):
|
||||
if Config().is_cluster_enabled(): return
|
||||
cookie_path = self.get_cookie_path()
|
||||
|
||||
if path.exists(cookie_path):
|
||||
with open(self.get_cookie_path(), 'rb') as f:
|
||||
cookie = pickle.load(f)
|
||||
self.cookie = True
|
||||
self.session.cookies.update(cookie)
|
||||
self.did_loaded_user()
|
||||
return True
|
||||
return None
|
||||
|
||||
def load_user_from_remote(self):
|
||||
cookie = self.cluster.get_user_cookie(self.key)
|
||||
info = self.cluster.get_user_info(self.key)
|
||||
if Config().is_slave() and (not cookie or not info):
|
||||
while True: # 子节点只能取
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_USER_COOKIE_NOT_FOUND_FROM_REMOTE.format(self.user_name)).flush()
|
||||
stay_second(self.retry_time)
|
||||
return self.load_user_from_remote()
|
||||
if info: self.info = info
|
||||
if cookie:
|
||||
self.session.cookies.update(cookie)
|
||||
if not self.cookie: # 第一次加载
|
||||
self.cookie = True
|
||||
if not Config().is_slave():
|
||||
self.did_loaded_user()
|
||||
else:
|
||||
self.is_ready = True # 设置子节点用户 已准备好
|
||||
UserLog.print_welcome_user(self)
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_is_ready(self):
|
||||
return self.is_ready
|
||||
|
||||
def wait_for_ready(self):
|
||||
if self.is_ready: return self
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_WAIT_USER_INIT_COMPLETE.format(self.retry_time)).flush()
|
||||
stay_second(self.retry_time)
|
||||
return self.wait_for_ready()
|
||||
|
||||
def destroy(self):
|
||||
"""
|
||||
退出用户
|
||||
:return:
|
||||
"""
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_USER_BEING_DESTROY.format(self.user_name)).flush()
|
||||
self.is_alive = False
|
||||
|
||||
def response_login_check(self, response, **kwargs):
|
||||
if Config().is_master() and response.json().get('data.noLogin') == 'true': # relogin
|
||||
self.handle_login(expire=True)
|
||||
|
||||
def get_user_passengers(self):
|
||||
if self.passengers: return self.passengers
|
||||
response = self.session.post(API_USER_PASSENGERS)
|
||||
result = response.json()
|
||||
if result.get('data.normal_passengers'):
|
||||
self.passengers = result.get('data.normal_passengers')
|
||||
# 将乘客写入到文件
|
||||
with open(Config().USER_PASSENGERS_FILE % self.user_name, 'w', encoding='utf-8') as f:
|
||||
f.write(json.dumps(self.passengers, indent=4, ensure_ascii=False))
|
||||
return self.passengers
|
||||
else:
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_GET_USER_PASSENGERS_FAIL.format(
|
||||
result.get('messages', CommonLog.MESSAGE_RESPONSE_EMPTY_ERROR), self.retry_time)).flush()
|
||||
if Config().is_slave():
|
||||
self.load_user_from_remote() # 加载最新 cookie
|
||||
stay_second(self.retry_time)
|
||||
return self.get_user_passengers()
|
||||
|
||||
def get_passengers_by_members(self, members):
|
||||
"""
|
||||
获取格式化后的乘客信息
|
||||
:param members:
|
||||
:return:
|
||||
[{
|
||||
name: '项羽',
|
||||
type: 1,
|
||||
id_card: 0000000000000000000,
|
||||
type_text: '成人',
|
||||
enc_str: 'aaaaaa'
|
||||
}]
|
||||
"""
|
||||
self.get_user_passengers()
|
||||
results = []
|
||||
for member in members:
|
||||
is_member_code = is_number(member)
|
||||
if not is_member_code:
|
||||
if member[0] == "*":
|
||||
audlt = 1
|
||||
member = member[1:]
|
||||
else:
|
||||
audlt = 0
|
||||
child_check = array_dict_find_by_key_value(results, 'name', member)
|
||||
if not is_member_code and child_check:
|
||||
new_member = child_check.copy()
|
||||
new_member['type'] = UserType.CHILD
|
||||
new_member['type_text'] = dict_find_key_by_value(UserType.dicts, int(new_member['type']))
|
||||
else:
|
||||
if is_member_code:
|
||||
passenger = array_dict_find_by_key_value(self.passengers, 'code', member)
|
||||
else:
|
||||
passenger = array_dict_find_by_key_value(self.passengers, 'passenger_name', member)
|
||||
if audlt:
|
||||
passenger['passenger_type'] = UserType.ADULT
|
||||
if not passenger:
|
||||
UserLog.add_quick_log(
|
||||
UserLog.MESSAGE_USER_PASSENGERS_IS_INVALID.format(self.user_name, member)).flush()
|
||||
return False
|
||||
new_member = {
|
||||
'name': passenger.get('passenger_name'),
|
||||
'id_card': passenger.get('passenger_id_no'),
|
||||
'id_card_type': passenger.get('passenger_id_type_code'),
|
||||
'mobile': passenger.get('mobile_no'),
|
||||
'type': passenger.get('passenger_type'),
|
||||
'type_text': dict_find_key_by_value(UserType.dicts, int(passenger.get('passenger_type'))),
|
||||
'enc_str': passenger.get('allEncStr')
|
||||
}
|
||||
results.append(new_member)
|
||||
|
||||
return results
|
||||
|
||||
def request_init_dc_page(self):
|
||||
"""
|
||||
请求下单页面 拿到 token
|
||||
:return:
|
||||
"""
|
||||
data = {'_json_att': ''}
|
||||
response = self.session.post(API_INITDC_URL, data)
|
||||
html = response.text
|
||||
token = re.search(r'var globalRepeatSubmitToken = \'(.+?)\'', html)
|
||||
form = re.search(r'var ticketInfoForPassengerForm *= *(\{.+\})', html)
|
||||
order = re.search(r'var orderRequestDTO *= *(\{.+\})', html)
|
||||
# 系统忙,请稍后重试
|
||||
if html.find('系统忙,请稍后重试') != -1:
|
||||
OrderLog.add_quick_log(OrderLog.MESSAGE_REQUEST_INIT_DC_PAGE_FAIL).flush() # 重试无用,直接跳过
|
||||
return False
|
||||
try:
|
||||
self.global_repeat_submit_token = token.groups()[0]
|
||||
self.ticket_info_for_passenger_form = json.loads(form.groups()[0].replace("'", '"'))
|
||||
self.order_request_dto = json.loads(order.groups()[0].replace("'", '"'))
|
||||
except:
|
||||
return False # TODO Error
|
||||
|
||||
return True
|
||||
@@ -1,93 +0,0 @@
|
||||
from py12306.app import *
|
||||
from py12306.cluster.cluster import Cluster
|
||||
from py12306.helpers.event import Event
|
||||
from py12306.helpers.func import *
|
||||
from py12306.log.user_log import UserLog
|
||||
from py12306.user.job import UserJob
|
||||
|
||||
|
||||
@singleton
|
||||
class User:
|
||||
users = []
|
||||
user_accounts = []
|
||||
|
||||
retry_time = 3
|
||||
cluster = None
|
||||
|
||||
def __init__(self):
|
||||
self.cluster = Cluster()
|
||||
self.update_interval()
|
||||
self.update_user_accounts()
|
||||
|
||||
def update_user_accounts(self, auto=False, old=None):
|
||||
self.user_accounts = Config().USER_ACCOUNTS
|
||||
if auto:
|
||||
UserLog.add_quick_log(UserLog.MESSAGE_USERS_DID_CHANGED).flush()
|
||||
self.refresh_users(old)
|
||||
|
||||
def update_interval(self, auto=False):
|
||||
self.interval = Config().USER_HEARTBEAT_INTERVAL
|
||||
if auto: jobs_do(self.users, 'update_user')
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
app_available_check()
|
||||
self.start()
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
self.init_users()
|
||||
UserLog.print_init_users(users=self.users)
|
||||
# 多线程维护用户
|
||||
create_thread_and_run(jobs=self.users, callback_name='run', wait=Const.IS_TEST)
|
||||
|
||||
def init_users(self):
|
||||
for account in self.user_accounts:
|
||||
self.init_user(account)
|
||||
|
||||
def init_user(self, info):
|
||||
user = UserJob(info=info)
|
||||
self.users.append(user)
|
||||
return user
|
||||
|
||||
def refresh_users(self, old):
|
||||
for account in self.user_accounts:
|
||||
key = account.get('key')
|
||||
old_account = array_dict_find_by_key_value(old, 'key', key)
|
||||
if old_account and account != old_account:
|
||||
user = self.get_user(key)
|
||||
user.init_data(account)
|
||||
elif not old_account: # 新用户 添加到 多线程
|
||||
new_user = self.init_user(account)
|
||||
create_thread_and_run(jobs=new_user, callback_name='run', wait=Const.IS_TEST)
|
||||
|
||||
for account in old: # 退出已删除的用户
|
||||
if not array_dict_find_by_key_value(self.user_accounts, 'key', account.get('key')):
|
||||
Event().user_job_destroy({'key': account.get('key')})
|
||||
|
||||
@classmethod
|
||||
def is_empty(cls):
|
||||
self = cls()
|
||||
return not bool(self.users)
|
||||
|
||||
@classmethod
|
||||
def get_user(cls, key) -> UserJob:
|
||||
self = cls()
|
||||
for user in self.users:
|
||||
if user.key == key: return user
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_passenger_for_members(cls, members, key):
|
||||
"""
|
||||
检测乘客信息
|
||||
:param passengers
|
||||
:return:
|
||||
"""
|
||||
self = cls()
|
||||
|
||||
for user in self.users:
|
||||
assert isinstance(user, UserJob)
|
||||
if user.key == key and user.wait_for_ready():
|
||||
return user.get_passengers_by_members(members)
|
||||
@@ -1,54 +0,0 @@
|
||||
import requests
|
||||
from hashlib import md5
|
||||
|
||||
|
||||
class RKClient(object):
|
||||
|
||||
def __init__(self, username, password, soft_id, soft_key):
|
||||
self.username = username
|
||||
self.password = md5(password.encode('utf-8')).hexdigest()
|
||||
self.soft_id = soft_id
|
||||
self.soft_key = soft_key
|
||||
self.base_params = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'softid': self.soft_id,
|
||||
'softkey': self.soft_key,
|
||||
}
|
||||
self.headers = {
|
||||
'Connection': 'Keep-Alive',
|
||||
'Expect': '100-continue',
|
||||
'User-Agent': 'ben',
|
||||
}
|
||||
|
||||
def rk_create(self, image, im_type, timeout=20):
|
||||
"""
|
||||
im: 图片字节
|
||||
im_type: 题目类型
|
||||
"""
|
||||
params = {
|
||||
'typeid': im_type,
|
||||
'timeout': timeout,
|
||||
'image': image
|
||||
}
|
||||
params.update(self.base_params)
|
||||
r = requests.post('http://api.ruokuai.com/create.json', data=params, timeout=timeout)
|
||||
return r.json()
|
||||
|
||||
def rk_report_error(self, im_id):
|
||||
"""
|
||||
im_id:报错题目的ID
|
||||
"""
|
||||
params = {
|
||||
'id': im_id,
|
||||
}
|
||||
params.update(self.base_params)
|
||||
r = requests.post('http://api.ruokuai.com/reporterror.json', data=params, headers=self.headers)
|
||||
return r.json()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
rc = RKClient('username', 'password', 'soft_id', 'soft_key')
|
||||
# im = open('a.jpg', 'rb').read()
|
||||
# print rc.rk_create(im, 3040)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from flask import Blueprint, request, send_file
|
||||
from flask.json import jsonify
|
||||
from flask_jwt_extended import (jwt_required)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
|
||||
app = Blueprint('app', __name__)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
file = Config().WEB_ENTER_HTML_PATH
|
||||
result = ''
|
||||
with open(file, 'r', encoding='utf-8') as f:
|
||||
result = f.read()
|
||||
config = {
|
||||
'API_BASE_URL': '' # TODO 自定义 Host
|
||||
}
|
||||
result = re.sub(r'<script>[\s\S]*?<\/script>', '<script>window.config={}</script>'.format(json.dumps(config)),
|
||||
result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/app/menus', methods=['GET'])
|
||||
@jwt_required
|
||||
def menus():
|
||||
"""
|
||||
菜单列表
|
||||
"""
|
||||
menus = [
|
||||
{"id": 10, "name": "首页", "url": "/", "icon": "fa fa-tachometer-alt"},
|
||||
{"id": 20, "name": "用户管理", "url": "/user", "icon": "fa fa-user"},
|
||||
{"id": 30, "name": "查询任务", "url": "/query", "icon": "fa fa-infinity"},
|
||||
{"id": 40, "name": "实时日志", "url": "/log/realtime", "icon": "fa fa-signature"},
|
||||
{"id": 50, "name": "帮助", "url": "/help", "icon": "fa fa-search"}
|
||||
]
|
||||
return jsonify(menus)
|
||||
|
||||
|
||||
@app.route('/app/actions', methods=['GET'])
|
||||
@jwt_required
|
||||
def actions():
|
||||
"""
|
||||
操作列表
|
||||
"""
|
||||
actions = [
|
||||
{"text": "退出登录", "key": 'logout', "link": "", "icon": "fa fa-sign-out-alt"}
|
||||
]
|
||||
return jsonify(actions)
|
||||
@@ -1,51 +0,0 @@
|
||||
import linecache
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask.json import jsonify
|
||||
from flask_jwt_extended import (jwt_required)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import get_file_total_line_num, pick_file_lines
|
||||
from py12306.log.common_log import CommonLog
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
|
||||
log = Blueprint('log', __name__)
|
||||
|
||||
|
||||
@log.route('/log/output', methods=['GET'])
|
||||
@jwt_required
|
||||
def log_output():
|
||||
"""
|
||||
日志
|
||||
:return:
|
||||
"""
|
||||
last_line = int(request.args.get('line', 0))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
max_old = 200 # 取最新时 往后再取的数
|
||||
file = Config().OUT_PUT_LOG_TO_FILE_PATH
|
||||
res = []
|
||||
|
||||
if last_line == -1:
|
||||
total_line = get_file_total_line_num(file)
|
||||
last_line = total_line - max_old if total_line > max_old else 0
|
||||
ranges = range(last_line, last_line + max_old + limit)
|
||||
# limit = max_old + limit
|
||||
else:
|
||||
ranges = range(last_line, last_line + limit)
|
||||
|
||||
if Config().OUT_PUT_LOG_TO_FILE_ENABLED:
|
||||
with open(Config().OUT_PUT_LOG_TO_FILE_PATH, 'r', encoding='utf-8') as f:
|
||||
res = pick_file_lines(f, ranges)
|
||||
|
||||
# linecache.updatecache(file) # 使用 linecache windows 平台会出来编码问题 暂时弃用
|
||||
# for i in ranges:
|
||||
# tmp = linecache.getline(file, last_line + i)
|
||||
# if tmp != '': res.append(tmp)
|
||||
last_line += len(res)
|
||||
else:
|
||||
res = CommonLog.MESSAGE_OUTPUT_TO_FILE_IS_UN_ENABLE
|
||||
return jsonify({
|
||||
'last_line': last_line,
|
||||
'data': res
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
from flask import Blueprint, request
|
||||
from flask.json import jsonify
|
||||
from flask_jwt_extended import (jwt_required)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.query.job import Job
|
||||
from py12306.query.query import Query
|
||||
|
||||
query = Blueprint('query', __name__)
|
||||
|
||||
|
||||
@query.route('/query', methods=['GET'])
|
||||
@jwt_required
|
||||
def query_lists():
|
||||
"""
|
||||
查询任务列表
|
||||
:return:
|
||||
"""
|
||||
jobs = Query().jobs
|
||||
result = list(map(convert_job_to_info, jobs))
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
def convert_job_to_info(job: Job):
|
||||
return {
|
||||
'name': job.job_name,
|
||||
'left_dates': job.left_dates,
|
||||
'stations': job.stations,
|
||||
'members': job.members,
|
||||
'member_num': job.member_num,
|
||||
'allow_seats': job.allow_seats,
|
||||
'allow_train_numbers': job.allow_train_numbers,
|
||||
'except_train_numbers': job.except_train_numbers,
|
||||
'allow_less_member': job.allow_less_member,
|
||||
'passengers': job.passengers,
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
from flask import Blueprint, request
|
||||
from flask.json import jsonify
|
||||
from flask_jwt_extended import (jwt_required)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.query.query import Query
|
||||
from py12306.user.user import User
|
||||
|
||||
stat = Blueprint('stat', __name__)
|
||||
|
||||
|
||||
@stat.route('/stat/dashboard', methods=['GET'])
|
||||
@jwt_required
|
||||
def dashboard():
|
||||
"""
|
||||
状态统计
|
||||
任务数量,用户数量,查询次数
|
||||
节点信息(TODO)
|
||||
:return:
|
||||
"""
|
||||
from py12306.log.query_log import QueryLog
|
||||
query_job_count = len(Query().jobs)
|
||||
user_job_count = len(User().users)
|
||||
query_count = QueryLog().data.get('query_count')
|
||||
res = {
|
||||
'query_job_count': query_job_count,
|
||||
'user_job_count': user_job_count,
|
||||
'query_count': query_count,
|
||||
}
|
||||
if Config().CDN_ENABLED:
|
||||
from py12306.helpers.cdn import Cdn
|
||||
res['cdn_count'] = len(Cdn().available_items)
|
||||
return jsonify(res)
|
||||
|
||||
|
||||
@stat.route('/stat/cluster', methods=['GET'])
|
||||
@jwt_required
|
||||
def clusters():
|
||||
"""
|
||||
节点统计
|
||||
节点数量,主节点,子节点列表
|
||||
:return:
|
||||
"""
|
||||
from py12306.cluster.cluster import Cluster
|
||||
nodes = Cluster().nodes
|
||||
count = len(nodes)
|
||||
node_lists = list(nodes)
|
||||
master = [key for key, val in nodes.items() if int(val) == Cluster.KEY_MASTER]
|
||||
master = master[0] if master else ''
|
||||
|
||||
return jsonify({
|
||||
'master': master,
|
||||
'count': count,
|
||||
'node_lists': ', '.join(node_lists)
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
from flask import Blueprint, request
|
||||
from flask.json import jsonify
|
||||
from flask_jwt_extended import (jwt_required, create_access_token)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import str_to_time, timestamp_to_time
|
||||
from py12306.user.job import UserJob
|
||||
from py12306.user.user import User
|
||||
|
||||
user = Blueprint('user', __name__)
|
||||
|
||||
|
||||
@user.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""
|
||||
用户登录
|
||||
:return:
|
||||
"""
|
||||
username = request.json.get('username', None)
|
||||
password = request.json.get('password', None)
|
||||
if username and password and username == Config().WEB_USER.get('username') and password == Config().WEB_USER.get(
|
||||
'password'):
|
||||
access_token = create_access_token(identity=username)
|
||||
return jsonify(access_token=access_token)
|
||||
return jsonify({"msg": "用户名或密码错误"}), 422
|
||||
|
||||
|
||||
@user.route('/users', methods=['GET'])
|
||||
@jwt_required
|
||||
def users():
|
||||
"""
|
||||
用户任务列表
|
||||
:return:
|
||||
"""
|
||||
jobs = User().users
|
||||
result = list(map(convert_job_to_info, jobs))
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@user.route('/user/info', methods=['GET'])
|
||||
@jwt_required
|
||||
def user_info():
|
||||
"""
|
||||
获取用户信息
|
||||
:return:
|
||||
"""
|
||||
result = {
|
||||
'name': Config().WEB_USER.get('username')
|
||||
}
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
def convert_job_to_info(job: UserJob):
|
||||
return {
|
||||
'key': job.key,
|
||||
'user_name': job.user_name,
|
||||
'name': job.get_name(),
|
||||
'is_ready': job.is_ready,
|
||||
'is_loaded': job.user_loaded, # 是否成功加载 ready 是当前是否可用
|
||||
'last_heartbeat': timestamp_to_time(job.last_heartbeat) if job.last_heartbeat else '-',
|
||||
'login_num': job.login_num
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="60px" height="65px" viewBox="0 0 60 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.1 (67048) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>2754577-avatar-business-face-people</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="left-bar" transform="translate(-90.000000, -43.000000)">
|
||||
<g id="avatar">
|
||||
<g transform="translate(90.000000, 43.000000)">
|
||||
<g id="Group-3">
|
||||
<g id="2754577-avatar-business-face-people" transform="translate(0.000000, 0.000000)">
|
||||
<path d="M46.3055556,57.5277778 C46.3055556,57.5277778 46.4166667,58.3333333 46.5833333,59.6527778 C50.125,57.3055556 53.125,54.2222222 55.3888889,50.625 C54.9861111,49.8055556 54.5694444,49.1527778 54.0972222,48.7361111 C52.5694444,47.3888889 48.8472222,44.4583333 35.9722222,38.8055556 L30.9027778,38.7777778 L30.9027778,38.3472222 L26.2638889,38.3472222 L26.2638889,38.8055556 L24.0416667,38.8194444 C11.1666667,44.4583333 7.44444444,47.3888889 5.91666667,48.75 C5.44444444,49.1666667 5.01388889,49.8194444 4.625,50.6388889 C6.88888889,54.2361111 9.88888889,57.3194444 13.4305556,59.6666667 C13.5972222,58.3472222 13.7083333,57.5416667 13.7083333,57.5416667 C13.7083333,57.5416667 13.7083333,58.4027778 13.7083333,59.8472222 C18.4027778,62.8888889 24,64.6666667 30,64.6666667 C36.0138889,64.6666667 41.6111111,62.9027778 46.2916667,59.8472222 C46.3055556,58.3888889 46.3055556,57.5277778 46.3055556,57.5277778 Z" id="Path" fill="#B9DAF3"></path>
|
||||
<path d="M5.91666667,48.7361111 C7.44444444,47.3888889 11.1666667,44.4583333 24.0416667,38.8055556 L26.2638889,38.7916667 L26.2638889,38.3333333 L30.9027778,38.3333333 L30.9027778,38.7638889 L35.9722222,38.7916667 C48.8472222,44.4305556 52.5694444,47.3611111 54.0972222,48.7222222 C54.5694444,49.1388889 55,49.7916667 55.3888889,50.6111111 C58.2916667,46 59.9722222,40.5416667 59.9722222,34.6805556 C59.9583333,18.1388889 46.5416667,4.72222222 30,4.72222222 C13.4583333,4.72222222 0.0416666667,18.1388889 0.0416666667,34.6944444 C0.0416666667,40.5416667 1.72222222,46.0138889 4.625,50.625 C5.01388889,49.8055556 5.44444444,49.1527778 5.91666667,48.7361111 Z" id="Path" fill="#D1E6F7"></path>
|
||||
<path d="M30,21.7083333 L30,44.8612519 C32.2638889,44.875 34.5277778,43.8611111 36.25,41.7638889 L36.25,27.4861111 C36.25,23.625 33.125,21.7083333 30,21.7083333 Z" id="Path" fill="#EDD9B4"></path>
|
||||
<path d="M23.75,27.4861111 L23.75,41.75 C25.4722222,43.7777778 27.7361111,44.8333333 30,44.8472222 L30,21.7083333 C26.875,21.7083333 23.75,23.625 23.75,27.4861111 Z" id="Path" fill="#DCC5A1"></path>
|
||||
<path d="M36.25,27.4861111 L36.25,37.0416667 C33.8888889,38.7638889 31.6111111,39.7361111 30,39.7361111 C28.3888889,39.7361111 26.1111111,38.7638889 23.75,37.0416667 L23.75,27.4861111 C23.75,19.7777778 36.25,19.7777778 36.25,27.4861111" id="Path" fill="#BC9F82"></path>
|
||||
<polygon id="Path" fill="#89BCE5" points="36.25 38.9305556 41.1805556 41.1805556 34.9027778 49.8888889 30 44.8611111"></polygon>
|
||||
<polygon id="Path" fill="#89BCE5" points="23.75 38.9305556 18.8194444 41.1805556 25.0972222 49.8888889 30 44.8611111"></polygon>
|
||||
<path d="M30.1527778,33.125 C30.1944444,33.125 30.2222222,33.125 30.2638889,33.125 C33.3194444,32.9861111 30.7777778,36.5555556 34.6666667,34.4722222 C38.5555556,32.3888889 38.3888889,26.3194444 36.4444444,26.1805556 C34.5138889,26.0416667 32.9166667,27.4166667 30.1666667,27.4305556 L30.1388889,27.4305556 C30.0972222,27.4305556 30.0555556,27.4305556 30.0138889,27.4305556 L30.0138889,33.125 C30.0555556,33.125 30.0972222,33.125 30.1527778,33.125 Z" id="Path" fill="#EDD9B4"></path>
|
||||
<path d="M30.0111111,37.3611111 C30.0277778,37.3611111 30.0277778,37.3611111 30.0444444,37.3611111 C30.0277778,37.3611111 30.0277778,37.3611111 30.0111111,37.3611111 Z" id="Path" fill="#FDDECC"></path>
|
||||
<path d="M29.3888889,37.3083333 C29.3388889,37.2916667 29.2722222,37.2916667 29.2222222,37.275 C29.2722222,37.2916667 29.3388889,37.2916667 29.3888889,37.3083333 Z" id="Path" fill="#FDDECC"></path>
|
||||
<path d="M24.0416667,26.3194444 C22.0972222,26.4583333 21.5138889,32.25 25.4027778,34.3333333 C29.1666667,36.3472222 27.3333333,33.1944444 30,33.125 L30,27.4305556 C27.3055556,27.3888889 25.9583333,26.1805556 24.0416667,26.3194444 Z" id="Path" fill="#DCC5A1"></path>
|
||||
<path d="M29.5555556,37.3305556 C29.7083333,37.3472222 29.8611111,37.3638889 30,37.3638889 C29.8611111,37.3638889 29.7083333,37.3472222 29.5555556,37.3305556 Z" id="Path" fill="#FDDECC"></path>
|
||||
<g id="Group" transform="translate(12.777778, 1.805556)">
|
||||
<path d="M33.4722222,16.7083333 C32.8888889,16.5138889 32.2361111,16.7777778 31.6388889,17.3333333 C32.1944444,9.51388889 30.0277778,0.0555555556 17.2222222,0.0555555556 L17.2222222,24.5694444 C17.25,24.5694444 17.2916667,24.5694444 17.3333333,24.5694444 L17.375,24.5694444 C19.5416667,24.5416667 23.2638889,22.4027778 24.7638889,24.0972222 C27,26.6527778 25.2777778,31.4444444 23.3611111,32.7361111 C26,30.9027778 28.4722222,28.6111111 29.2916667,27.4166667 C29.5972222,26.9722222 30.1805556,25.4305556 30.7083333,23.2777778 C30.8472222,23.4166667 31.0277778,23.5277778 31.2083333,23.5972222 C32.3055556,23.9583333 33.7083333,22.7083333 34.3333333,20.8055556 C34.9583333,18.9027778 34.5694444,17.0694444 33.4722222,16.7083333 Z" id="Path" fill="#EDD9B4"></path>
|
||||
<path d="M2.77777778,17.0555556 C2.27777778,16.6944444 1.76388889,16.5555556 1.29166667,16.7083333 C0.194444444,17.0694444 -0.194444444,18.9027778 0.430555556,20.8055556 C1.05555556,22.7083333 2.45833333,23.9583333 3.55555556,23.5972222 C3.63888889,23.5694444 3.72222222,23.5277778 3.79166667,23.4861111 C4.30555556,25.5277778 4.86111111,27 5.15277778,27.4166667 C6,28.6666667 8.63888889,31.0833333 11.3888889,32.9444444 C8.75,30.7222222 7.69444444,26.4166667 10.0833333,24.1944444 C11.6805556,22.7083333 15.0555556,24.5 17.2222222,24.5555556 L17.2222222,0.0555555556 C4.56944444,0.0555555556 2.29166667,9.29166667 2.77777778,17.0555556 Z" id="Path" fill="#DCC5A1"></path>
|
||||
</g>
|
||||
<path d="M29.5555556,37.3361111 C29.4861111,37.3194444 29.4305556,37.3194444 29.3611111,37.3027778 C29.4305556,37.3194444 29.5,37.3194444 29.5555556,37.3361111 Z" id="Path" fill="#0B0C14"></path>
|
||||
<path d="M30.1527778,37.3597222 C30.1666667,37.3597222 30.1805556,37.3597222 30.1805556,37.3597222 C30.125,37.3597222 30.0694444,37.3763889 30.0138889,37.3763889 C30.0694444,37.3597222 30.1111111,37.3597222 30.1527778,37.3597222 Z" id="Path" fill="#0B0C14"></path>
|
||||
<path d="M36.1263889,34.5833333 C36.1430556,34.5833333 36.1430556,34.5666667 36.1597222,34.55 C36.1263889,34.5666667 36.1097222,34.5833333 36.0763889,34.6166667 C36.0930556,34.6166667 36.1097222,34.6 36.1263889,34.5833333 Z" id="Path" fill="#0B0C14"></path>
|
||||
<path d="M37.5416667,25.9166667 C36.0416667,24.2083333 32.3194444,26.3472222 30.1527778,26.3888889 L30.1111111,26.3888889 C30.0833333,26.3888889 30.0416667,26.3888889 30,26.3888889 C27.8333333,26.3333333 24.4583333,24.5277778 22.8611111,26.0277778 C20.4722222,28.25 21.5277778,32.5555556 24.1666667,34.7777778 C25.9583333,36 27.7916667,36.9861111 29.2222222,37.2777778 C29.2638889,37.2916667 29.3194444,37.3055556 29.3611111,37.3055556 C29.4305556,37.3194444 29.4861111,37.3194444 29.5555556,37.3333333 C29.7083333,37.3472222 29.8611111,37.3611111 30,37.3611111 C30.0138889,37.3611111 30.0138889,37.3611111 30.0277778,37.3611111 C30.0833333,37.3611111 30.1388889,37.3611111 30.1944444,37.3472222 C31.7222222,37.25 33.9166667,36.0138889 36.0694444,34.6111111 C36.0972222,34.5972222 36.1111111,34.5833333 36.1388889,34.5555556 C38.0555556,33.2638889 39.7777778,28.4722222 37.5416667,25.9166667 Z M34.6666667,34.4722222 C30.7777778,36.5555556 33.3194444,32.9861111 30.2638889,33.125 C30.2222222,33.125 30.1944444,33.125 30.1527778,33.125 C30.0972222,33.125 30.0555556,33.125 30,33.125 C27.3333333,33.1944444 29.1666667,36.3472222 25.4027778,34.3333333 C21.5138889,32.25 22.0972222,26.4583333 24.0416667,26.3194444 C25.9583333,26.1805556 27.3055556,27.3888889 30,27.4305556 C30.0416667,27.4305556 30.0833333,27.4305556 30.125,27.4305556 L30.1527778,27.4305556 C32.9166667,27.4166667 34.5,26.0416667 36.4305556,26.1805556 C38.375,26.3194444 38.5416667,32.3888889 34.6666667,34.4722222 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
|
||||
<path d="M30.0277778,9.06944444 C23.5416667,9.09722222 17.4722222,8.80555556 16.2361111,23.0277778 C13.0833333,11.9027778 17.3888889,-0.0277777778 29.7916667,0.0694444444 C42.625,0.180555556 47.2638889,11.9861111 43.7222222,23.0416667 C40.8333333,7.77777778 35.9027778,9.04166667 30.0277778,9.06944444" id="Path" fill="#000000"></path>
|
||||
<g id="Group" transform="translate(23.055556, 18.750000)">
|
||||
<path d="M1.48611111,0.0833333333 C2.25,0.0833333333 2.875,0.791666667 2.875,1.65277778 C2.875,2.52777778 2.25,3.22222222 1.48611111,3.22222222 C0.722222222,3.22222222 0.0972222222,2.51388889 0.0972222222,1.65277778 C0.0972222222,0.791666667 0.708333333,0.0833333333 1.48611111,0.0833333333" id="Path" fill="#342214"></path>
|
||||
<path d="M12.7361111,0.0833333333 C13.5,0.0833333333 14.125,0.791666667 14.125,1.65277778 C14.125,2.52777778 13.5,3.22222222 12.7361111,3.22222222 C11.9722222,3.22222222 11.3472222,2.51388889 11.3472222,1.65277778 C11.3472222,0.791666667 11.9722222,0.0833333333 12.7361111,0.0833333333" id="Path" fill="#342214"></path>
|
||||
<path d="M4.25,10.0277778 C4.73611111,11.1111111 5.83333333,11.875 7.11111111,11.875 C8.38888889,11.875 9.47222222,11.1111111 9.97222222,10.0277778 L4.25,10.0277778 Z" id="Path" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
<path d="M13.7232639,59.8333333 C13.7232639,58.3888889 13.7232639,57.5277778 13.7232639,57.5277778 C13.7065972,57.5555556 13.7065972,58.4027778 13.7232639,59.8333333 C13.7232639,59.8333333 13.7232639,59.8333333 13.7232639,59.8333333 Z" id="Path" fill="#89BCE5"></path>
|
||||
<path d="M46.2916667,59.8333333 C46.3888889,59.7777778 46.4861111,59.7083333 46.5694444,59.6527778 C46.4027778,58.3333333 46.2916667,57.5277778 46.2916667,57.5277778 C46.2916667,57.5277778 46.3055556,58.3888889 46.2916667,59.8333333 Z" id="Path" fill="#89BCE5"></path>
|
||||
<path d="M13.6944444,57.5277778 C13.6944444,57.5277778 13.5833333,58.3194444 13.4166667,59.6527778 C13.5833333,58.3472222 13.6944444,57.5555556 13.6944444,57.5277778 Z" id="Path" fill="#89BCE5"></path>
|
||||
<path d="M13.7083333,59.8333333 C13.7083333,58.4027778 13.7083333,57.5555556 13.7083333,57.5277778 C13.7083333,57.5416667 13.5972222,58.3472222 13.4305556,59.6527778 C13.5138889,59.7083333 13.6111111,59.7777778 13.7083333,59.8333333 Z" id="Path" fill="#89BCE5"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 729 KiB |
@@ -1,471 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!--
|
||||
Font Awesome Free 5.6.0 by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
-->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<font id="fontawesome-free" horiz-adv-x="640">
|
||||
<font-face font-family="Font Awesome 5 Free"
|
||||
units-per-em="512" ascent="448"
|
||||
descent="64"
|
||||
font-weight="400"
|
||||
font-style="Regular" />
|
||||
<missing-glyph horiz-adv-x="0" />
|
||||
<glyph glyph-name="address-book"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M436 288C442.6 288 448 293.4 448 300V340C448 346.6 442.6 352 436 352H416V400C416 426.5 394.5 448 368 448H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H368C394.5 -64 416 -42.5 416 -16V32H436C442.6 32 448 37.4 448 44V84C448 90.6 442.6 96 436 96H416V160H436C442.6 160 448 165.4 448 172V212C448 218.6 442.6 224 436 224H416V288H436zM368 -16H48V400H368V-16zM208 192C243.3 192 272 220.7 272 256S243.3 320 208 320S144 291.3 144 256S172.7 192 208 192zM118.4 64H297.6C310 64 320 72.6 320 83.2V102.4C320 134.2 289.9 160 252.8 160C242 160 234.1 152 208 152C181.1 152 174.6 160 163.2 160C126.1 160 96 134.2 96 102.4V83.2C96 72.6 106 64 118.4 64z" />
|
||||
<glyph glyph-name="address-card"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M528 416H48C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48 -32H528C554.5 -32 576 -10.5 576 16V368C576 394.5 554.5 416 528 416zM528 16H48V368H528V16zM208 192C243.3 192 272 220.7 272 256S243.3 320 208 320S144 291.3 144 256S172.7 192 208 192zM118.4 64H297.6C310 64 320 72.6 320 83.2V102.4C320 134.2 289.9 160 252.8 160C242 160 234.1 152 208 152C181.1 152 174.6 160 163.2 160C126.1 160 96 134.2 96 102.4V83.2C96 72.6 106 64 118.4 64zM360 128H472C476.4 128 480 131.6 480 136V152C480 156.4 476.4 160 472 160H360C355.6 160 352 156.4 352 152V136C352 131.6 355.6 128 360 128zM360 192H472C476.4 192 480 195.6 480 200V216C480 220.4 476.4 224 472 224H360C355.6 224 352 220.4 352 216V200C352 195.6 355.6 192 360 192zM360 256H472C476.4 256 480 259.6 480 264V280C480 284.4 476.4 288 472 288H360C355.6 288 352 284.4 352 280V264C352 259.6 355.6 256 360 256z" />
|
||||
<glyph glyph-name="angry"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM248 136C214.4 136 182.8 121.2 161.2 95.4C152.7 85.2 154.1 70.1 164.3 61.6S189.6 54.4 198.1 64.6C222.9 94.3 273.1 94.3 297.9 64.6C306 54.9 321.1 52.7 331.7 61.6C341.9 70.1 343.2 85.2 334.8 95.4C313.2 121.2 281.6 136 248 136zM200 208C210.3 208 219.9 214.7 223 225.1C226.8 237.8 219.6 251.2 206.9 255L126.9 279C114.1 282.9 100.8 275.6 97 262.9C93.2 250.2 100.4 236.8 113.1 233L141.3 224.5C138.2 219.6 136 214.1 136 207.9C136 190.2 150.3 175.9 168 175.9S200 190.3 200 208zM399 262.9C395.2 275.6 381.9 282.8 369.1 279L289.1 255C276.4000000000001 251.2 269.2000000000001 237.8 273 225.1C276.1 214.7 285.7 208 296 208C296 190.3 310.3 176 328 176S360 190.3 360 208C360 214.2 357.8 219.7 354.7 224.6L382.9 233.1C395.6 236.8 402.8 250.2 399 262.9z" />
|
||||
<glyph glyph-name="arrow-alt-circle-down"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119 440 8 329 8 192S119 -56 256 -56S504 55 504 192S393 440 256 440zM256 -8C145.5 -8 56 81.5 56 192S145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8zM224 308V192H157C146.3 192 141 179.1 148.5 171.5L247.5 72.5C252.2 67.8 259.8 67.8 264.5 72.5L363.5 171.5C371.1 179.1 365.7 192 355 192H288V308C288 314.6 282.6 320 276 320H236C229.4 320 224 314.6 224 308z" />
|
||||
<glyph glyph-name="arrow-alt-circle-left"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M8 192C8 55 119 -56 256 -56S504 55 504 192S393 440 256 440S8 329 8 192zM456 192C456 81.5 366.5 -8 256 -8S56 81.5 56 192S145.5 392 256 392S456 302.5 456 192zM384 212V172C384 165.4 378.6 160 372 160H256V93C256 82.3 243.1 77 235.5 84.5L136.5 183.5C131.8 188.2 131.8 195.8 136.5 200.5L235.5 299.5C243.1 307.1 256 301.7 256 291V224H372C378.6 224 384 218.6 384 212z" />
|
||||
<glyph glyph-name="arrow-alt-circle-right"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M504 192C504 329 393 440 256 440S8 329 8 192S119 -56 256 -56S504 55 504 192zM56 192C56 302.5 145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8S56 81.5 56 192zM128 172V212C128 218.6 133.4 224 140 224H256V291C256 301.7 268.9 307 276.5 299.5L375.5 200.5C380.2 195.8 380.2 188.2 375.5 183.5L276.5 84.5C268.9 76.9 256 82.3 256 93V160H140C133.4 160 128 165.4 128 172z" />
|
||||
<glyph glyph-name="arrow-alt-circle-up"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 -56C393 -56 504 55 504 192S393 440 256 440S8 329 8 192S119 -56 256 -56zM256 392C366.5 392 456 302.5 456 192S366.5 -8 256 -8S56 81.5 56 192S145.5 392 256 392zM276 64H236C229.4 64 224 69.4 224 76V192H157C146.3 192 141 204.9 148.5 212.5L247.5 311.5C252.2 316.2 259.8 316.2 264.5 311.5L363.5 212.5C371.1 204.9 365.7 192 355 192H288V76C288 69.4 282.6 64 276 64z" />
|
||||
<glyph glyph-name="bell-slash"
|
||||
unicode=""
|
||||
horiz-adv-x="640" d=" M633.99 -23.02L36 444.49C29.1 450.01 19.03 448.9 13.51 442L3.51 429.51C-2.02 422.61 -0.9 412.54 6 407.02L604 -60.49C610.9 -66.01 620.96 -64.89 626.49 -58L636.49 -45.51C642.01 -38.61 640.9 -28.54 633.99 -23.02zM163.53 80C180.24 102.03 198.01 135.8 204.93 190.58L159.46 226.13C156.19 135.4 122.99 105.45 104.62 85.71C98.62 79.2600000000001 95.96 71.55 96.01 64.0000000000001C96.12 47.6000000000001 108.99 32.0000000000001 128.11 32.0000000000001H407.7700000000001L346.3700000000001 80.0000000000001H163.53zM320 352C381.86 352 432 301.86 432 240C432 239.8 431.94 239.62 431.94 239.42C431.96 222.58 433.1 207.65 434.73 193.69L494.26 147.15C485.95 169.28 479.92 198.64 479.92 240C479.92 317.7 425.44 379.9 351.98 395.16V416C351.98 433.67 337.6600000000001 448 320 448S288.02 433.67 288.02 416V395.16C262 389.75 238.57 378.2200000000001 218.89 362.44L257.06 332.6C275 344.82 296.65 352 320 352zM320 -64C355.32 -64 383.9700000000001 -35.35 383.9700000000001 0H256.03C256.03 -35.35 284.68 -64 320 -64z" />
|
||||
<glyph glyph-name="bell"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M439.39 85.71C420.07 106.47 383.92 137.7 383.92 240C383.92 317.7 329.44 379.9 255.98 395.16V416C255.98 433.67 241.66 448 224 448S192.02 433.67 192.02 416V395.16C118.56 379.9 64.08 317.7 64.08 240C64.08 137.7 27.93 106.47 8.61 85.71C2.61 79.2600000000001 -0.05 71.55 0 64.0000000000001C0.11 47.6000000000001 12.98 32.0000000000001 32.1 32.0000000000001H415.9000000000001C435.0200000000001 32.0000000000001 447.9000000000001 47.6000000000001 448.0000000000001 64.0000000000001C448.0500000000001 71.5500000000001 445.3900000000001 79.27 439.3900000000001 85.71zM67.53 80C88.75 107.97 111.95 154.33 112.06 239.42C112.06 239.62 112 239.8 112 240C112 301.86 162.14 352 224 352S336 301.86 336 240C336 239.8 335.94 239.62 335.94 239.42C336.05 154.32 359.25 107.96 380.4700000000001 80H67.53zM224 -64C259.32 -64 287.9700000000001 -35.35 287.9700000000001 0H160.03C160.03 -35.35 188.68 -64 224 -64z" />
|
||||
<glyph glyph-name="bookmark"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M336 448H48C21.49 448 0 426.51 0 400V-64L192 48L384 -64V400C384 426.51 362.51 448 336 448zM336 19.57L192 103.57L48 19.57V394A6 6 0 0 0 54 400H330C333.314 400 336 397.317 336 394.004V19.57z" />
|
||||
<glyph glyph-name="building"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M128 300V340C128 346.6 133.4 352 140 352H180C186.6 352 192 346.6 192 340V300C192 293.4 186.6 288 180 288H140C133.4 288 128 293.4 128 300zM268 288H308C314.6 288 320 293.4 320 300V340C320 346.6 314.6 352 308 352H268C261.4 352 256 346.6 256 340V300C256 293.4 261.4 288 268 288zM140 192H180C186.6 192 192 197.4 192 204V244C192 250.6 186.6 256 180 256H140C133.4 256 128 250.6 128 244V204C128 197.4 133.4 192 140 192zM268 192H308C314.6 192 320 197.4 320 204V244C320 250.6 314.6 256 308 256H268C261.4 256 256 250.6 256 244V204C256 197.4 261.4 192 268 192zM192 108V148C192 154.6 186.6 160 180 160H140C133.4 160 128 154.6 128 148V108C128 101.4 133.4 96 140 96H180C186.6 96 192 101.4 192 108zM268 96H308C314.6 96 320 101.4 320 108V148C320 154.6 314.6 160 308 160H268C261.4 160 256 154.6 256 148V108C256 101.4 261.4 96 268 96zM448 -28V-64H0V-28C0 -21.4 5.4 -16 12 -16H31.5V424C31.5 437.3 42.2 448 55.5 448H392.5C405.8 448 416.5 437.3 416.5 424V-16H436C442.6 -16 448 -21.4 448 -28zM79.5 -15H192V52C192 58.6 197.4 64 204 64H244C250.6 64 256 58.6 256 52V-15H368.5V399L80 400L79.5 -15z" />
|
||||
<glyph glyph-name="calendar-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M400 384H352V432C352 440.8 344.8 448 336 448H304C295.2 448 288 440.8 288 432V384H160V432C160 440.8 152.8 448 144 448H112C103.2 448 96 440.8 96 432V384H48C21.5 384 0 362.5 0 336V-16C0 -42.5 21.5 -64 48 -64H400C426.5 -64 448 -42.5 448 -16V336C448 362.5 426.5 384 400 384zM400 288V208H304V288H400zM176 96V176H272V96H176zM272 64V-16H176V64H272zM144 96H48V176H144V96zM176 208V288H272V208H176zM304 176H400V96H304V176zM144 288V208H48V288H144zM48 -10V64H144V-16H54C50.7 -16 48 -13.3 48 -10zM394 -16H304V64H400V-10C400 -13.3 397.3 -16 394 -16z" />
|
||||
<glyph glyph-name="calendar-check"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M400 384H352V436C352 442.627 346.627 448 340 448H300C293.373 448 288 442.627 288 436V384H160V436C160 442.627 154.627 448 148 448H108C101.373 448 96 442.627 96 436V384H48C21.49 384 0 362.51 0 336V-16C0 -42.51 21.49 -64 48 -64H400C426.51 -64 448 -42.51 448 -16V336C448 362.51 426.51 384 400 384zM394 -16H54A6 6 0 0 0 48 -10V288H400V-10A6 6 0 0 0 394 -16zM341.151 184.65L198.842 43.481C194.137 38.814 186.539 38.844 181.871 43.549L106.78 119.248C102.113 123.953 102.143 131.551 106.848 136.219L129.567 158.755C134.272 163.422 141.87 163.392 146.537 158.686L190.641 114.225L301.713 224.406C306.418 229.073 314.016 229.043 318.6840000000001 224.3379999999999L341.2200000000001 201.62C345.887 196.9149999999999 345.8560000000001 189.317 341.151 184.65z" />
|
||||
<glyph glyph-name="calendar-minus"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M124 120C117.4 120 112 125.4 112 132V156C112 162.6 117.4 168 124 168H324C330.6 168 336 162.6 336 156V132C336 125.4 330.6 120 324 120H124zM448 336V-16C448 -42.5 426.5 -64 400 -64H48C21.5 -64 0 -42.5 0 -16V336C0 362.5 21.5 384 48 384H96V436C96 442.6 101.4 448 108 448H148C154.6 448 160 442.6 160 436V384H288V436C288 442.6 293.4 448 300 448H340C346.6 448 352 442.6 352 436V384H400C426.5 384 448 362.5 448 336zM400 -10V288H48V-10C48 -13.3 50.7 -16 54 -16H394C397.3 -16 400 -13.3 400 -10z" />
|
||||
<glyph glyph-name="calendar-plus"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M336 156V132C336 125.4 330.6 120 324 120H248V44C248 37.4 242.6 32 236 32H212C205.4 32 200 37.4 200 44V120H124C117.4 120 112 125.4 112 132V156C112 162.6 117.4 168 124 168H200V244C200 250.6 205.4 256 212 256H236C242.6 256 248 250.6 248 244V168H324C330.6 168 336 162.6 336 156zM448 336V-16C448 -42.5 426.5 -64 400 -64H48C21.5 -64 0 -42.5 0 -16V336C0 362.5 21.5 384 48 384H96V436C96 442.6 101.4 448 108 448H148C154.6 448 160 442.6 160 436V384H288V436C288 442.6 293.4 448 300 448H340C346.6 448 352 442.6 352 436V384H400C426.5 384 448 362.5 448 336zM400 -10V288H48V-10C48 -13.3 50.7 -16 54 -16H394C397.3 -16 400 -13.3 400 -10z" />
|
||||
<glyph glyph-name="calendar-times"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M311.7 73.3L294.7 56.3C290 51.6 282.4 51.6 277.7 56.3L224 110.1L170.3 56.4C165.6 51.7 158 51.7 153.3 56.4L136.3 73.4C131.6 78.1 131.6 85.7000000000001 136.3 90.4L190 144.1L136.3 197.8C131.6 202.5 131.6 210.1 136.3 214.8L153.3 231.8C158 236.5 165.6 236.5 170.3 231.8L224 178.1L277.7 231.8C282.4 236.5 290 236.5 294.7 231.8L311.7 214.8C316.4 210.1 316.4 202.5 311.7 197.8L257.9 144L311.6 90.3C316.4 85.6 316.4 78 311.7 73.3zM448 336V-16C448 -42.5 426.5 -64 400 -64H48C21.5 -64 0 -42.5 0 -16V336C0 362.5 21.5 384 48 384H96V436C96 442.6 101.4 448 108 448H148C154.6 448 160 442.6 160 436V384H288V436C288 442.6 293.4 448 300 448H340C346.6 448 352 442.6 352 436V384H400C426.5 384 448 362.5 448 336zM400 -10V288H48V-10C48 -13.3 50.7 -16 54 -16H394C397.3 -16 400 -13.3 400 -10z" />
|
||||
<glyph glyph-name="calendar"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M400 384H352V436C352 442.6 346.6 448 340 448H300C293.4 448 288 442.6 288 436V384H160V436C160 442.6 154.6 448 148 448H108C101.4 448 96 442.6 96 436V384H48C21.5 384 0 362.5 0 336V-16C0 -42.5 21.5 -64 48 -64H400C426.5 -64 448 -42.5 448 -16V336C448 362.5 426.5 384 400 384zM394 -16H54C50.7 -16 48 -13.3 48 -10V288H400V-10C400 -13.3 397.3 -16 394 -16z" />
|
||||
<glyph glyph-name="caret-square-down"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M125.1 240H322.9C333.6 240 339 227 331.4 219.5L232.5 121.2C227.8 116.5 220.3 116.5 215.6 121.2L116.7 219.5C109 227 114.4 240 125.1 240zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="caret-square-left"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M272 290.9V93.1C272 82.4 259 77 251.5 84.6L153.2 183.5C148.5 188.2 148.5 195.7 153.2 200.4L251.5 299.3C259 307 272 301.6 272 290.9zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="caret-square-right"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M176 93.1V290.9C176 301.6 189 307 196.5 299.4L294.8 200.5C299.5 195.8 299.5 188.3 294.8 183.6L196.5 84.7000000000001C189 77.0000000000001 176 82.4 176 93.1zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="caret-square-up"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M322.9 144H125.1C114.4 144 109 157 116.6 164.5L215.5 262.8C220.2 267.5 227.7 267.5 232.4 262.8L331.3 164.5C339 157 333.6 144 322.9000000000001 144zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="chart-bar"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M396.8 96H419.2C425.6 96 432 102.4 432 108.8V339.2C432 345.6 425.6 352 419.2 352H396.8C390.4000000000001 352 384 345.6 384 339.2V108.8C384 102.4 390.4 96 396.8 96zM204.8 96H227.2C233.6 96 240.0000000000001 102.4 240.0000000000001 108.8V307.2C240.0000000000001 313.6 233.6 320 227.2 320H204.8C198.4 320 192 313.6 192 307.2V108.8C192 102.4 198.4 95.9999999999999 204.8 95.9999999999999zM300.8 96H323.2C329.6 96 336 102.4 336 108.8V243.2C336 249.6 329.6 256 323.2 256H300.8C294.4000000000001 256 288 249.6 288 243.2V108.8C288 102.4 294.4 95.9999999999999 300.8 95.9999999999999zM496 48H48V368C48 376.8400000000001 40.84 384 32 384H16C7.16 384 0 376.8400000000001 0 368V32C0 14.33 14.33 0 32 0H496C504.84 0 512 7.16 512 16V32C512 40.84 504.84 48 496 48zM108.8 96H131.2C137.6 96 144 102.4 144 108.8V179.2000000000001C144 185.6 137.6 192.0000000000001 131.2 192.0000000000001H108.8C102.4 192.0000000000001 96 185.6000000000001 96 179.2000000000001V108.8000000000001C96 102.4000000000001 102.4 96.0000000000001 108.8 96.0000000000001z" />
|
||||
<glyph glyph-name="check-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119.033 440 8 328.967 8 192S119.033 -56 256 -56S504 55.033 504 192S392.967 440 256 440zM256 392C366.532 392 456 302.549 456 192C456 81.468 366.549 -8 256 -8C145.468 -8 56 81.451 56 192C56 302.532 145.451 392 256 392M396.204 261.733L373.668 284.451C369.0010000000001 289.156 361.403 289.187 356.698 284.519L215.346 144.303L155.554 204.58C150.887 209.285 143.289 209.316 138.584 204.649L115.865 182.113C111.16 177.446 111.129 169.848 115.797 165.142L206.578 73.6259999999999C211.245 68.9209999999999 218.843 68.8899999999999 223.548 73.5579999999999L396.1370000000001 244.762C400.8410000000001 249.43 400.8710000000001 257.0279999999999 396.2040000000001 261.733z" />
|
||||
<glyph glyph-name="check-square"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M400 416H48C21.49 416 0 394.51 0 368V16C0 -10.51 21.49 -32 48 -32H400C426.51 -32 448 -10.51 448 16V368C448 394.51 426.51 416 400 416zM400 16H48V368H400V16zM364.136 257.724L191.547 86.52C186.842 81.853 179.244 81.883 174.577 86.588L83.796 178.104C79.129 182.809 79.159 190.407 83.865 195.075L106.584 217.611C111.289 222.278 118.887 222.248 123.554 217.542L183.346 157.265L324.698 297.481C329.403 302.148 337.001 302.118 341.668 297.413L364.204 274.695C368.871 269.9890000000001 368.841 262.391 364.136 257.724z" />
|
||||
<glyph glyph-name="circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119 440 8 329 8 192S119 -56 256 -56S504 55 504 192S393 440 256 440zM256 -8C145.5 -8 56 81.5 56 192S145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8z" />
|
||||
<glyph glyph-name="clipboard"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M336 384H256C256 419.29 227.29 448 192 448S128 419.29 128 384H48C21.49 384 0 362.51 0 336V-16C0 -42.51 21.49 -64 48 -64H336C362.51 -64 384 -42.51 384 -16V336C384 362.51 362.51 384 336 384zM330 -16H54A6 6 0 0 0 48 -10V330A6 6 0 0 0 54 336H96V300C96 293.373 101.373 288 108 288H276C282.627 288 288 293.373 288 300V336H330A6 6 0 0 0 336 330V-10A6 6 0 0 0 330 -16zM192 408C205.255 408 216 397.255 216 384S205.255 360 192 360S168 370.745 168 384S178.745 408 192 408" />
|
||||
<glyph glyph-name="clock"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119 440 8 329 8 192S119 -56 256 -56S504 55 504 192S393 440 256 440zM256 -8C145.5 -8 56 81.5 56 192S145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8zM317.8 96.4L232.9 158.1C229.8 160.4 228 164 228 167.8V332C228 338.6 233.4 344 240 344H272C278.6 344 284 338.6 284 332V190.3L350.8 141.7C356.2 137.8 357.3 130.3 353.4000000000001 124.9L334.6 99C330.7000000000001 93.7 323.2000000000001 92.5 317.8 96.4z" />
|
||||
<glyph glyph-name="clone"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 448H144C117.49 448 96 426.51 96 400V352H48C21.49 352 0 330.51 0 304V-16C0 -42.51 21.49 -64 48 -64H368C394.51 -64 416 -42.51 416 -16V32H464C490.51 32 512 53.49 512 80V400C512 426.51 490.51 448 464 448zM362 -16H54A6 6 0 0 0 48 -10V298A6 6 0 0 0 54 304H96V80C96 53.49 117.49 32 144 32H368V-10A6 6 0 0 0 362 -16zM458 80H150A6 6 0 0 0 144 86V394A6 6 0 0 0 150 400H458A6 6 0 0 0 464 394V86A6 6 0 0 0 458 80z" />
|
||||
<glyph glyph-name="closed-captioning"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 384H48C21.5 384 0 362.5 0 336V48C0 21.5 21.5 0 48 0H464C490.5 0 512 21.5 512 48V336C512 362.5 490.5 384 464 384zM458 48H54C50.7 48 48 50.7 48 54V330C48 333.3 50.7 336 54 336H458C461.3 336 464 333.3 464 330V54C464 50.7 461.3 48 458 48zM246.9 133.7C248.6 131.3 248.4 128.1 246.4 126C192.8 69.2 73.6 93.9 73.6 193.9C73.6 291.2 195.3 313.4 246.1 264C248.2 262 248.6 260.8 247.1 258.3L229.6 227.8C227.7 224.7 223.4 223.8 220.5 226.1C179.7 258.1 125.9 241.0000000000001 125.9 194.9C125.9 146.9 176.9 124.4 218.1 162.3C220.9 164.8 225.2 164.4 227.3 161.4L246.9 133.7000000000001zM437.3 133.7C439 131.3 438.8 128.1 436.8 126C383.2 69.1 264 93.9 264 193.9C264 291.2 385.7 313.4 436.5 264C438.6 262 439 260.8 437.5 258.3L420 227.8C418.1 224.7 413.8 223.8 410.9 226.1C370.1 258.1 316.3 241.0000000000001 316.3 194.9C316.3 146.9 367.3 124.4 408.5 162.3C411.3 164.8 415.6 164.4 417.7 161.4L437.3 133.7000000000001z" />
|
||||
<glyph glyph-name="comment-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M448 448H64C28.7 448 0 419.3 0 384V96C0 60.7 28.7 32 64 32H160V-52C160 -59.1 165.8 -64 172 -64C174.4 -64 176.9 -63.3 179.1 -61.6L304 32H448C483.3 32 512 60.7 512 96V384C512 419.3 483.3 448 448 448zM464 96C464 87.2 456.8 80 448 80H288L275.2 70.4L208 20V80H64C55.2 80 48 87.2 48 96V384C48 392.8 55.2 400 64 400H448C456.8 400 464 392.8 464 384V96z" />
|
||||
<glyph glyph-name="comment-dots"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M144 240C126.3 240 112 225.7 112 208S126.3 176 144 176S176 190.3 176 208S161.7 240 144 240zM256 240C238.3 240 224 225.7 224 208S238.3 176 256 176S288 190.3 288 208S273.7 240 256 240zM368 240C350.3 240 336 225.7 336 208S350.3 176 368 176S400 190.3 400 208S385.7 240 368 240zM256 416C114.6 416 0 322.9 0 208C0 160.4 19.9 116.8 52.9 81.7C38 42.3 7 8.9 6.5 8.5C-0.1 1.5 -1.9 -8.7 1.9 -17.5S14.4 -32 24 -32C85.5 -32 134 -6.3 163.1 14.3C192 5.2 223.2 0 256 0C397.4 0 512 93.1 512 208S397.4 416 256 416zM256 48C229.3 48 202.9 52.1 177.6 60.1L154.9 67.3L135.4 53.5C121.1 43.4 101.5 32.1 77.9 24.5C85.2 36.6 92.3 50.2 97.8 64.7L108.4 92.8L87.8 114.6C69.7 133.9 48 165.8 48 208C48 296.2 141.3 368 256 368S464 296.2 464 208S370.7 48 256 48z" />
|
||||
<glyph glyph-name="comment"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 416C114.6 416 0 322.9 0 208C0 160.4 19.9 116.8 52.9 81.7C38 42.3 7 8.9 6.5 8.5C-0.1 1.5 -1.9 -8.7 1.9 -17.5S14.4 -32 24 -32C85.5 -32 134 -6.3 163.1 14.3C192 5.2 223.2 0 256 0C397.4 0 512 93.1 512 208S397.4 416 256 416zM256 48C229.3 48 202.9 52.1 177.6 60.1L154.9 67.3L135.4 53.5C121.1 43.4 101.5 32.1 77.9 24.5C85.2 36.6 92.3 50.2 97.8 64.7L108.4 92.8L87.8 114.6C69.7 133.9 48 165.8 48 208C48 296.2 141.3 368 256 368S464 296.2 464 208S370.7 48 256 48z" />
|
||||
<glyph glyph-name="comments"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M532 61.8C559.5 88.9 576 122.9 576 160C576 240 499.5 306.1 399.8 317.9C368.3 375.5 294.3 416 208 416C93.1 416 0 344.4 0 256C0 219 16.5 185 44 157.8C28.7 127.1 6.7 103.3 6.3 102.9C0 96.2000000000001 -1.8 86.4 1.9 77.9C5.5 69.4 13.9 63.9 23.1 63.9C76.6 63.9 119.8 84.1 148.3 102.7000000000001C157.5 100.6 167 99.0000000000001 176.7 97.8000000000001C208.1 40.4 281.8 0 368 0C388.8 0 408.8 2.4 427.8 6.8C456.3 -11.7 499.4 -32 553 -32C562.2 -32 570.5 -26.5 574.2 -18C577.8000000000001 -9.5 576.1 0.3 569.8000000000001 7C569.4000000000001 7.3 547.3000000000001 31.1 532.0000000000001 61.8zM139.2 154.1L122.1 143C108 133.9 93.6 126.7 79 121.6C81.7 126.3 84.4 131.3 87 136.4L102.5 167.5000000000001L77.7 192C64.2 205.4 48 227.3 48 256C48 316.7 121.3 368 208 368S368 316.7 368 256S294.7 144 208 144C191.5 144 175 145.9 159 149.6L139.2 154.1zM498.3 96L473.6 71.6L489.1 40.5C491.7 35.4 494.4 30.4 497.1 25.7C482.5 30.8 468.1 38 454 47.1L436.9 58.2L417 53.6C401 49.9 384.5 47.9999999999999 368 47.9999999999999C314 47.9999999999999 265.8 68.1 236.7 97.6999999999999C338 108.5 416 175.1 416 256C416 259.4 415.6 262.7 415.3 266C479.7 251.5 528 209.2 528 160C528 131.3 511.8 109.4 498.3 96z" />
|
||||
<glyph glyph-name="compass"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M347.94 318.14L203.6 252.17A31.938 31.938 0 0 1 187.83 236.4L121.86 92.06C114.25 75.41 131.4 58.2499999999999 148.06 65.86L292.4 131.8299999999999A31.938 31.938 0 0 1 308.17 147.5999999999999L374.14 291.94C381.75 308.5999999999999 364.6 325.7499999999999 347.94 318.1399999999999zM270.58 169.42C258.11 156.9499999999999 237.89 156.9499999999999 225.42 169.42C212.95 181.89 212.95 202.11 225.42 214.58C237.89 227.05 258.11 227.05 270.58 214.58C283.05 202.11 283.05 181.89 270.58 169.42zM248 440C111.03 440 0 328.9700000000001 0 192S111.03 -56 248 -56S496 55.03 496 192S384.9700000000001 440 248 440zM248 -8C137.72 -8 48 81.72 48 192S137.72 392 248 392S448 302.28 448 192S358.28 -8 248 -8z" />
|
||||
<glyph glyph-name="copy"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M433.941 382.059L382.059 433.941A48 48 0 0 1 348.118 448H176C149.49 448 128 426.51 128 400V352H48C21.49 352 0 330.51 0 304V-16C0 -42.51 21.49 -64 48 -64H272C298.51 -64 320 -42.51 320 -16V32H400C426.51 32 448 53.49 448 80V348.118A48 48 0 0 1 433.941 382.059zM266 -16H54A6 6 0 0 0 48 -10V298A6 6 0 0 0 54 304H128V80C128 53.49 149.49 32 176 32H272V-10A6 6 0 0 0 266 -16zM394 80H182A6 6 0 0 0 176 86V394A6 6 0 0 0 182 400H288V312C288 298.745 298.745 288 312 288H400V86A6 6 0 0 0 394 80zM400 336H336V400H345.632C347.223 400 348.749 399.368 349.875 398.243L398.243 349.875A6 6 0 0 0 400 345.632V336z" />
|
||||
<glyph glyph-name="copyright"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119.033 440 8 328.967 8 192S119.033 -56 256 -56S504 55.033 504 192S392.967 440 256 440zM256 -8C145.468 -8 56 81.451 56 192C56 302.531 145.451 392 256 392C366.532 392 456 302.549 456 192C456 81.468 366.549 -8 256 -8zM363.351 93.064C353.737 83.352 317.8210000000001 51.668 259.286 51.668C176.856 51.668 118.802 113.093 118.802 193.235C118.802 272.387 179.077 332.636 258.564 332.636C314.095 332.636 347.302 306.016 356.157 297.857A11.965 11.965 0 0 0 358.093 282.535L339.938 254.422C336.097 248.472 327.972 247.14 322.439 251.501C313.844 258.277 290.625 274.039 260.731 274.039C212.428 274.039 182.815 238.709 182.815 193.957C182.815 152.368 209.703 110.265 261.092 110.265C293.749 110.265 317.935 129.304 326.818 137.49C332.088 142.347 340.414 141.529 344.638 135.752L364.503 108.582A11.947000000000001 11.947000000000001 0 0 0 363.351 93.064z" />
|
||||
<glyph glyph-name="credit-card"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M527.9 416H48.1C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48.1 -32H527.9C554.5 -32 576 -10.5 576 16V368C576 394.5 554.5 416 527.9 416zM54.1 368H521.9C525.1999999999999 368 527.9 365.3 527.9 362V320H48.1V362C48.1 365.3 50.8 368 54.1 368zM521.9 16H54.1C50.8 16 48.1 18.7 48.1 22V192H527.9V22C527.9 18.7 525.1999999999999 16 521.9 16zM192 116V76C192 69.4 186.6 64 180 64H108C101.4 64 96 69.4 96 76V116C96 122.6 101.4 128 108 128H180C186.6 128 192 122.6 192 116zM384 116V76C384 69.4 378.6 64 372 64H236C229.4 64 224 69.4 224 76V116C224 122.6 229.4 128 236 128H372C378.6 128 384 122.6 384 116z" />
|
||||
<glyph glyph-name="dizzy"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM214.2 209.9C222 217.7 222 230.4 214.2 238.2L196.3 256L214.2 273.9C222 281.7000000000001 222 294.4 214.2 302.2000000000001C206.4 310 193.7 310 185.9 302.2000000000001L168 284.3L150.2 302.1C142.4 309.9000000000001 129.7 309.9000000000001 121.9 302.1C114.1 294.3 114.1 281.6 121.9 273.8L139.8 255.9L121.9 238C114.1 230.2 114.1 217.5 121.9 209.7C129.7 201.9 142.4 201.9 150.2 209.7L168 227.5L185.8 209.7C193.7 202 206.3 202 214.2 209.9zM374.2 302.1C366.4 309.9000000000001 353.7 309.9000000000001 345.9 302.1L328 284.3L310.2 302.1C302.4 309.9000000000001 289.7 309.9000000000001 281.9 302.1C274.1 294.3 274.1 281.6 281.9 273.8L299.8 255.9L281.9 238C274.1 230.2 274.1 217.5 281.9 209.7C289.7 201.9 302.4 201.9 310.2 209.7L328 227.5L345.8 209.7C353.6 201.9 366.3 201.9 374.1 209.7C381.9000000000001 217.5 381.9000000000001 230.2 374.1 238L356.3 256L374.2 273.9C381.9 281.7000000000001 381.9 294.3 374.2 302.1zM248 176C212.7 176 184 147.3 184 112S212.7 48 248 48S312 76.7 312 112S283.3 176 248 176z" />
|
||||
<glyph glyph-name="dot-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 392C366.532 392 456 302.549 456 192C456 81.468 366.549 -8 256 -8C145.468 -8 56 81.451 56 192C56 302.532 145.451 392 256 392M256 440C119.033 440 8 328.967 8 192S119.033 -56 256 -56S504 55.033 504 192S392.967 440 256 440zM256 272C211.817 272 176 236.183 176 192S211.817 112 256 112S336 147.817 336 192S300.183 272 256 272z" />
|
||||
<glyph glyph-name="edit"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M402.3 103.1L434.3 135.1C439.3 140.1 448 136.6 448 129.4V-16C448 -42.5 426.5 -64 400 -64H48C21.5 -64 0 -42.5 0 -16V336C0 362.5 21.5 384 48 384H321.5C328.6 384 332.2 375.4 327.2 370.3L295.2 338.3C293.7 336.8 291.7 336 289.5 336H48V-16H400V97.5C400 99.6 400.8 101.6 402.3 103.1zM558.9 304.9000000000001L296.3 42.3L205.9 32.3C179.7 29.4 157.4 51.5 160.3 77.9L170.3 168.3000000000001L432.9 430.9C455.8 453.8 492.8 453.8 515.6 430.9L558.8000000000001 387.7C581.7 364.8 581.7 327.7 558.9000000000001 304.9zM460.1 274L402 332.1L216.2 146.2L208.9 80.9L274.2 88.2L460.1 274zM524.9 353.7L481.7 396.9C477.6 401 470.9 401 466.9 396.9L436 366L494.1 307.9L525 338.8C529 343 529 349.6 524.9 353.7000000000001z" />
|
||||
<glyph glyph-name="envelope-open"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M494.586 283.4840000000001C489.889 287.367 382.863 373.4340000000001 359.3350000000001 392.141C337.231 409.809 299.437 448 256 448C212.795 448 175.364 410.283 152.665 392.141C128.202 372.6910000000001 21.595 286.9460000000001 17.515 283.592A48.004000000000005 48.004000000000005 0 0 1 0 246.515V-16C0 -42.51 21.49 -64 48 -64H464C490.51 -64 512 -42.51 512 -16V246.491A48 48 0 0 1 494.586 283.4840000000001zM464 -10A6 6 0 0 0 458 -16H54A6 6 0 0 0 48 -10V243.653C48 245.466 48.816 247.179 50.226 248.318C66.096 261.132 159.019 335.872 182.59 354.611C200.755 369.12 232.398 400 256 400C279.693 400 311.857 368.631 329.41 354.611C352.983 335.87 445.913 261.118 461.776 248.295A5.99 5.99 0 0 0 463.9999999999999 243.632V-10zM432.009 177.704C436.2580000000001 172.545 435.474 164.909 430.264 160.723C401.289 137.44 370.99 113.126 359.3350000000001 103.86C336.636 85.717 299.205 48 256 48C212.548 48 174.713 86.237 152.665 103.86C141.386 112.827 110.921 137.273 81.738 160.725C76.528 164.912 75.745 172.547 79.993 177.706L95.251 196.234C99.429 201.307 106.908 202.077 112.03 197.96C140.648 174.959 170.596 150.925 182.59 141.389C200.143 127.369 232.307 96 256 96C279.602 96 311.246 126.88 329.41 141.389C341.404 150.924 371.354 174.959 399.973 197.957C405.095 202.073 412.574 201.303 416.751 196.23L432.009 177.704z" />
|
||||
<glyph glyph-name="envelope"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 384H48C21.49 384 0 362.51 0 336V48C0 21.49 21.49 0 48 0H464C490.51 0 512 21.49 512 48V336C512 362.51 490.51 384 464 384zM464 336V295.195C441.578 276.936 405.832 248.544 329.413 188.705C312.572 175.458 279.212 143.633 256 144.004C232.792 143.629 199.421 175.463 182.587 188.705C106.18 248.535 70.425 276.933 48 295.195V336H464zM48 48V233.602C70.914 215.351 103.409 189.74 152.938 150.956C174.795 133.751 213.072 95.77 256 96.001C298.717 95.77 336.509 133.2000000000001 359.053 150.948C408.581 189.7310000000001 441.085 215.3490000000001 464 233.6010000000001V48H48z" />
|
||||
<glyph glyph-name="eye-slash"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M272.702 88.861C192.219 97.872 136.49 175.747 155.772 255.903L272.702 88.861zM288 56C185.444 56 95.908 110.701 48 192C69.755 228.917 100.1 260.342 136.344 283.658L108.803 323.0010000000001C67.001 295.766 31.921 259.259 6.646 216.369A47.999 47.999 0 0 1 6.646 167.63C63.004 71.994 168.14 8 288 8A332.89 332.89 0 0 1 327.648 10.367L295.627 56.111A284.16 284.16 0 0 0 288 56zM569.354 167.631C536.1220000000001 111.237 485.933 65.889 425.8000000000001 38.139L473.9160000000001 -30.601C477.717 -36.03 476.3960000000001 -43.513 470.967 -47.313L450.23 -61.83C444.8010000000001 -65.631 437.3180000000001 -64.3099999999999 433.5180000000001 -58.881L102.084 414.601C98.283 420.03 99.604 427.513 105.033 431.313L125.77 445.83C131.199 449.631 138.682 448.31 142.482 442.881L198.008 363.556C226.612 371.657 256.808 376 288 376C407.86 376 512.996 312.006 569.354 216.369A48.00200000000001 48.00200000000001 0 0 0 569.354 167.631zM528 192C483.843 266.933 404.323 319.27 311.838 327.007C302.042 316.9220000000001 296 303.17 296 288C296 257.072 321.072 232 352 232S408 257.072 408 288L407.999 288.0420000000001C438.6310000000001 230.765 424.738 157.7820000000001 371.0710000000001 116.323L397.766 78.188C452.626 101.449 498.308 141.614 528 192z" />
|
||||
<glyph glyph-name="eye"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M569.354 216.369C512.97 312.051 407.81 376 288 376C168.14 376 63.004 312.006 6.646 216.369A47.999 47.999 0 0 1 6.646 167.63C63.031 71.949 168.19 8 288 8C407.86 8 512.996 71.994 569.354 167.631A47.997 47.997 0 0 1 569.354 216.369zM288 56C185.444 56 95.909 110.701 48 192C92.157 266.933 171.677 319.27 264.1620000000001 327.007C273.9580000000001 316.9220000000001 280 303.17 280 288C280 257.072 254.928 232 224 232S168 257.072 168 288L168.001 288.0420000000001C157.794 268.957 152 247.156 152 224C152 148.889 212.889 88 288 88S424 148.889 424 224C424 255.031 413.6 283.629 396.105 306.515C451.704 283.362 498.009 242.894 528 192C480.092 110.701 390.5560000000001 56 288 56z" />
|
||||
<glyph glyph-name="file-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M288 200V172C288 165.4 282.6 160 276 160H108C101.4 160 96 165.4 96 172V200C96 206.6 101.4 212 108 212H276C282.6 212 288 206.6 288 200zM276 128H108C101.4 128 96 122.6 96 116V88C96 81.4 101.4 76 108 76H276C282.6 76 288 81.4 288 88V116C288 122.6 282.6 128 276 128zM384 316.1V-16C384 -42.5 362.5 -64 336 -64H48C21.5 -64 0 -42.5 0 -16V400C0 426.5 21.5 448 48 448H252.1C264.8 448 277 442.9 286 433.9L369.9 350C378.9 341.1 384 328.8 384 316.1zM256 396.1V320H332.1L256 396.1zM336 -16V272H232C218.7 272 208 282.7 208 296V400H48V-16H336z" />
|
||||
<glyph glyph-name="file-archive"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.941 350.059L286.059 433.9410000000001A48 48 0 0 1 252.118 448H48C21.49 448 0 426.51 0 400V-16C0 -42.51 21.49 -64 48 -64H336C362.51 -64 384 -42.51 384 -16V316.118A48 48 0 0 1 369.941 350.059zM256 396.118L332.118 320H256V396.118zM336 -16H48V400H127.714V384H159.714V400H208V296C208 282.745 218.745 272 232 272H336V-16zM192.27 352H160.27V384H192.27V352zM160.27 352V320H128.27V352H160.27zM160.27 288V256H128.27V288H160.27zM192.27 288H160.27V320H192.27V288zM194.179 182.322A12 12 0 0 1 182.406 192H160.27V224H128.27V192L108.58 94.894C101.989 62.389 126.834 32 160 32C193.052 32 217.871 62.192 211.476 94.62L194.179 182.322zM160.27 57.927C142.352 57.927 127.826 70.032 127.826 84.963C127.826 99.895 142.351 111.999 160.27 111.999S192.714 99.894 192.714 84.963C192.714 70.032 178.188 57.927 160.27 57.927zM192.27 224H160.27V256H192.27V224z" />
|
||||
<glyph glyph-name="file-audio"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.941 350.059L286.059 433.9410000000001A48 48 0 0 1 252.118 448H48C21.49 448 0 426.51 0 400V-16C0 -42.51 21.49 -64 48 -64H336C362.51 -64 384 -42.51 384 -16V316.118A48 48 0 0 1 369.941 350.059zM332.118 320H256V396.118L332.118 320zM48 -16V400H208V296C208 282.745 218.745 272 232 272H336V-16H48zM192 60.024C192 49.333 179.074 43.979 171.515 51.539L136 87.514H108C101.373 87.514 96 92.887 96 99.514V155.514C96 162.141 101.373 167.514 108 167.514H136L171.515 204.461C179.075 212.021 192 206.667 192 195.976V60.024zM233.201 107.154C242.252 116.451 242.261 131.287 233.202 140.593C211.053 163.345 245.437 196.839 267.597 174.074C294.795 146.134 294.809 101.63 267.598 73.673C245.805 51.287 210.651 83.988 233.201 107.154z" />
|
||||
<glyph glyph-name="file-code"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.941 350.059L286.059 433.9410000000001A48 48 0 0 1 252.118 448H48C21.49 448 0 426.51 0 400V-16C0 -42.51 21.49 -64 48 -64H336C362.51 -64 384 -42.51 384 -16V316.118A48 48 0 0 1 369.941 350.059zM332.118 320H256V396.118L332.118 320zM48 -16V400H208V296C208 282.745 218.745 272 232 272H336V-16H48zM149.677 99.115L116.854 128L149.676 156.885A8.793 8.793 0 0 1 150.281 169.509L132.878 188.073C129.494 191.686 123.914 191.735 120.44 188.4740000000001L62.78 134.42C59.077 130.946 59.076 125.053 62.781 121.5800000000001L120.44 67.525A8.738 8.738 0 0 1 126.452 65.1440000000001A8.746 8.746 0 0 1 132.879 67.9260000000001L150.282 86.489A8.795 8.795 0 0 1 149.677 99.115zM233.961 226.965L209.56 234.049A8.796 8.796 0 0 1 198.655 228.051L144.04 39.939C142.687 35.279 145.378 30.387 150.038 29.0340000000001L174.441 21.95C179.121 20.595 183.998 23.304 185.346 27.948L239.958 216.06C241.312 220.72 238.621 225.612 233.961 226.9650000000001zM321.219 134.42L263.561 188.475C260.035 191.7820000000001 254.462 191.6400000000001 251.122 188.074L233.719 169.511A8.795 8.795 0 0 1 234.324 156.886L267.146 128L234.324 99.115A8.793 8.793 0 0 1 233.719 86.491L251.122 67.927A8.797 8.797 0 0 1 263.5610000000001 67.526H263.5600000000001L321.2200000000001 121.581C324.923 125.054 324.923 130.947 321.2190000000001 134.42z" />
|
||||
<glyph glyph-name="file-excel"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48zM260 224H231.2C226.8 224 222.8 221.6 220.7 217.7C202.7 184.6 198.5 175.3 192.1 160C178.2 189.1 185.2 177.3 163.5 217.7C161.4 221.6 157.3 224 152.9 224H124C114.7 224 109 214 113.6 206L159.9 128L113.6 50C108.9 42 114.7 32 124 32H152.9C157.3 32 161.3 34.4 163.4 38.3C185.1 78.3 186.4 83.3 192 96C206.9 65.8 197.9 80.1 220.6 38.3C222.7 34.4 226.8 32 231.2 32H260C269.3 32 275 42 270.4 50L224 128C224.7 129.1 254.3 178.5 270.3 206C275 214 269.2 224 260 224z" />
|
||||
<glyph glyph-name="file-image"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48zM80 32H304V160L280.5 183.5C275.8 188.2 268.2 188.2 263.5 183.5L176 96L136.5 135.5C131.8 140.2 124.2 140.2 119.5 135.5L80 96V32zM128 272C101.5 272 80 250.5 80 224S101.5 176 128 176S176 197.5 176 224S154.5 272 128 272z" />
|
||||
<glyph glyph-name="file-pdf"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48zM298.2 127.7C286 139.7 251.2 136.4 233.8 134.2C216.6 144.7 205.1 159.2 197 180.5C200.9 196.6 207.1 221.1 202.4 236.5C198.2 262.7 164.6 260.1 159.8 242.4C155.4 226.3 159.4 203.9 166.8 175.3C156.8 151.4 141.9 119.3 131.4 100.9C111.4 90.6 84.4 74.7 80.4 54.7C77.1 38.9 106.4 -0.5 156.5 85.9C178.9 93.3 203.3 102.4 224.9 106C243.8 95.8 265.9 89 280.7 89C306.2 89 308.7 117.2 298.2 127.7zM100.1 49.9C105.2 63.6 124.6 79.4 130.5 84.9C111.5 54.6 100.1 49.2 100.1 49.9zM181.7 240.5C189.1 240.5 188.4 208.4 183.5 199.7C179.1 213.6 179.2 240.5 181.7 240.5zM157.3 103.9C167 120.8 175.3 140.9 182 158.6C190.3 143.5 200.9 131.4 212.1 123.1C191.3 118.8 173.2 109.9999999999999 157.3 103.9zM288.9 108.9S283.9 102.9 251.6 116.7C286.7 119.3 292.5 111.3 288.9 108.9z" />
|
||||
<glyph glyph-name="file-powerpoint"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48zM120 44V212C120 218.6 125.4 224 132 224H201.2C237.9 224 264 197 264 157.7C264 83.4 195.3 91.2 168.5 91.2V44C168.5 37.4 163.1 32 156.5 32H132C125.4 32 120 37.4 120 44zM168.5 131.4H191.5C199.4 131.4 205.4 133.8 209.6 138.6C218.1 148.4 218 167.1 209.7 176.4C205.6 181 199.8 183.4 192.3 183.4H168.4V131.4z" />
|
||||
<glyph glyph-name="file-video"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.941 350.059L286.059 433.9410000000001A48 48 0 0 1 252.118 448H48C21.49 448 0 426.51 0 400V-16C0 -42.51 21.49 -64 48 -64H336C362.51 -64 384 -42.51 384 -16V316.118A48 48 0 0 1 369.941 350.059zM332.118 320H256V396.118L332.118 320zM48 -16V400H208V296C208 282.745 218.745 272 232 272H336V-16H48zM276.687 195.303L224 142.626V180C224 191.046 215.046 200 204 200H100C88.954 200 80 191.046 80 180V76C80 64.954 88.954 56 100 56H204C215.046 56 224 64.954 224 76V113.374L276.687 60.7000000000001C286.704 50.682 304 57.72 304 72.014V183.989C304 198.3 286.691 205.308 276.687 195.303z" />
|
||||
<glyph glyph-name="file-word"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48zM268.1 192C262.4000000000001 192 257.5 188 256.4000000000001 182.5C235.8000000000001 84.8 236.0000000000001 87.1 235.4000000000001 79C235.2000000000001 80.2 235.0000000000001 81.6 234.7000000000001 83.3C233.9000000000001 88.4 235.0000000000001 83.1 211.1000000000001 182.8C209.8000000000001 188.2 205.0000000000001 192 199.4000000000001 192H186.1000000000001C180.6000000000001 192 175.8000000000001 188.2 174.4000000000001 182.9C150.0000000000001 83.9 150.4000000000001 86.7 149.6000000000001 79.2C149.5000000000001 80.3 149.4000000000001 81.7 149.1000000000001 83.4C148.4000000000001 88.6 135.0000000000001 156.7 130.0000000000001 182.4C128.9000000000001 188 124.0000000000001 192.1 118.2000000000001 192.1H101.4000000000001C93.6000000000001 192.1 87.9000000000001 184.8 89.7000000000001 177.3C97.7000000000001 144.6999999999999 116.4000000000001 67.8 122.9000000000001 41.3C124.2000000000001 35.9 129.0000000000001 32.1999999999999 134.6000000000001 32.1999999999999H159.8000000000001C165.3000000000001 32.1999999999999 170.1000000000001 35.8999999999999 171.4 41.3L189.3000000000001 112.6999999999999C190.8000000000001 118.8999999999999 191.8000000000001 124.6999999999999 192.3000000000001 130L195.2000000000001 112.6999999999999C195.3000000000001 112.3 207.8000000000001 62.1999999999999 213.1000000000001 41.3C214.4000000000001 35.9999999999999 219.2000000000001 32.1999999999999 224.7000000000001 32.1999999999999H249.4000000000001C254.9000000000001 32.1999999999999 259.7000000000001 35.8999999999999 261.0000000000001 41.3C281.8000000000001 123.1999999999999 291.2000000000001 160.3 295.5000000000001 177.3C297.4000000000001 184.9 291.7000000000001 192.2 283.9000000000001 192.2H268.1z" />
|
||||
<glyph glyph-name="file"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M369.9 350.1L286 434C277 443 264.8 448.1 252.1 448.1H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V316.1C384 328.8 378.9 341.1 369.9 350.1zM332.1 320H256V396.1L332.1 320zM48 -16V400H208V296C208 282.7 218.7 272 232 272H336V-16H48z" />
|
||||
<glyph glyph-name="flag"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M336.174 368C287.042 368 242.869 400 174.261 400C142.96 400 115.958 393.5180000000001 93.54 384.832A48.04 48.04 0 0 1 95.682 405.559C93.067 428.425 74.167 446.406 51.201 447.896C23.242 449.71 0 427.569 0 400C0 382.236 9.657 366.738 24 358.438V-48C24 -56.837 31.163 -64 40 -64H56C64.837 -64 72 -56.837 72 -48V35.443C109.869 52.72 143.259 64 199.826 64C248.958 64 293.131 32 361.7390000000001 32C420.218 32 463.711 54.617 490.287 71.981C503.846 80.839 512 95.949 512 112.145V352.063C512 386.522 476.736 409.831 445.096 396.18C409.193 380.6910000000001 371.641 368 336.174 368zM464 112C442.217 96.588 403.176 80 361.7390000000001 80C301.7940000000001 80 259.737 112 199.826 112C156.465 112 103.447 102.597 72 88V320C93.784 335.4120000000001 132.824 352 174.261 352C234.206 352 276.2630000000001 320 336.1740000000001 320C379.4450000000001 320 432.4940000000001 337.366 464 352V112z" />
|
||||
<glyph glyph-name="flushed"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM344 304C299.8 304 264 268.2 264 224S299.8 144 344 144S424 179.8 424 224S388.2 304 344 304zM344 176C317.5 176 296 197.5 296 224S317.5 272 344 272S392 250.5 392 224S370.5 176 344 176zM344 248C330.7 248 320 237.3 320 224S330.7 200 344 200S368 210.7 368 224S357.3 248 344 248zM232 224C232 268.2 196.2 304 152 304S72 268.2 72 224S107.8 144 152 144S232 179.8 232 224zM152 176C125.5 176 104 197.5 104 224S125.5 272 152 272S200 250.5 200 224S178.5 176 152 176zM152 248C138.7 248 128 237.3 128 224S138.7 200 152 200S176 210.7 176 224S165.3 248 152 248zM312 104H184C170.8 104 160 93.2 160 80S170.8 56 184 56H312C325.2 56 336 66.8 336 80S325.2 104 312 104z" />
|
||||
<glyph glyph-name="folder-open"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M527.943 224H480V272C480 298.51 458.51 320 432 320H272L208 384H48C21.49 384 0 362.51 0 336V48C0 21.49 21.49 0 48 0H448A48.001 48.001 0 0 1 488.704 22.56L568.646 150.56C588.5939999999999 182.477 565.608 224 527.943 224zM54 336H188.118L252.118 272H426A6 6 0 0 0 432 266V224H152A48 48 0 0 1 110.902 200.798L48 96.551V330.007A5.993 5.993 0 0 0 54 336zM448 48H72L149.234 176H528L448 48z" />
|
||||
<glyph glyph-name="folder"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 320H272L217.37 374.63C211.37 380.63 203.23 384 194.74 384H48C21.49 384 0 362.51 0 336V48C0 21.49 21.49 0 48 0H464C490.51 0 512 21.49 512 48V272C512 298.51 490.51 320 464 320zM464 48H48V336H188.12L242.75 281.37C248.75 275.37 256.89 272 265.38 272H464V48z" />
|
||||
<glyph glyph-name="frown-open"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM200 240C200 257.7 185.7 272 168 272S136 257.7 136 240S150.3 208 168 208S200 222.3 200 240zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272zM248 160C212.4 160 159.2 138.7 152.2 98.8C150.2 87 161.2 77.3 172.7 80.7C203.9 90.3 232.1 96 248 96S292.1 90.3 323.3 80.7C334.7 77.2 345.8 87 343.8 98.8C336.8 138.7 283.6 160 248 160z" />
|
||||
<glyph glyph-name="frown"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272zM248 144C207.8 144 170 126.3 144.2 95.4C135.7 85.2 137.1 70.1 147.3 61.6C157.5 53.2 172.6 54.4999999999999 181.1 64.7C197.7 84.6 222.1 96.1 248 96.1S298.3 84.7 314.9 64.7C323 55 338 52.8 348.7 61.6C358.9 70.1 360.2 85.2 351.8 95.4C326 126.3 288.2 144 248 144z" />
|
||||
<glyph glyph-name="futbol"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M483.8 268.6C449.8 373.4 352.6 440 248.1 440C222.7 440 196.9 436.1 171.4 427.8C41.2 385.5 -30.1 245.6 12.2 115.4C46.2 10.6 143.4 -56 247.9 -56C273.3 -56 299.1 -52.1 324.6 -43.8C454.8 -1.5 526.1 138.4 483.8 268.6zM409.3 74.9L357.1 68.5L313.4000000000001 129.4L337.8 204.6L408.9 226.7L447.8 190.3C447.6 159.6 440.4 129.1999999999999 426.1 101.1C421.4 91.8 415.4 83.3 409.3 74.9zM409.3 310.3L398.9000000000001 257.2L328.2000000000001 235.2L264.0000000000001 281.7V355.5L311.4000000000001 381.7C350.6 368.7 384.8000000000001 343.7 409.3000000000001 310.3zM184.9 381.6L232 355.5V281.7L167.8 235.2L97.2 257.2L87.1 309.7C111.4 343.1 145 368.3 184.9 381.6zM139 68.5L85.9 75C71.5 95.1 48.6 134.6 48.1 190.3L87.1 226.7L158.2 204.5L182.5 130.2000000000001L139 68.5000000000001zM187.2 1.5L164.8 49.6L208.4 111.3H287L331.3 49.6L308.9000000000001 1.5C302.7000000000001 -0.3 251.3000000000001 -18.9 187.2000000000001 1.5z" />
|
||||
<glyph glyph-name="gem"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M464 448H112C108 448 104.2 446 102 442.6L2 295.4C-0.9 291 -0.6 285.2000000000001 2.7 281.2000000000001L278.7 -59.6C283.5 -65.5 292.5 -65.5 297.3 -59.6L573.3 281.2C576.5999999999999 285.3 576.9 291 574 295.4L474.1 442.6C471.8 446 468.1 448 464 448zM444.7 400L508 304H439.6L387.9000000000001 400H444.7000000000001zM242.6 400H333.3L385 304H191L242.6 400zM131.3 400H188.1L136.4 304H68L131.3 400zM88.3 256H139.7L208 96L88.3 256zM191.2 256H384.8L288 12.7L191.2 256zM368 96L436.2 256H487.6L368 96z" />
|
||||
<glyph glyph-name="grimace"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 208C345.7 208 360 222.3 360 240S345.7 272 328 272S296 257.7 296 240S310.3 208 328 208zM344 192H152C125.5 192 104 170.5 104 144V112C104 85.5 125.5 64 152 64H344C370.5 64 392 85.5 392 112V144C392 170.5 370.5 192 344 192zM176 96H152C143.2 96 136 103.2 136 112V120H176V96zM176 136H136V144C136 152.8 143.2 160 152 160H176V136zM240 96H192V120H240V96zM240 136H192V160H240V136zM304 96H256V120H304V96zM304 136H256V160H304V136zM360 112C360 103.2 352.8 96 344 96H320V120H360V112zM360 136H320V160H344C352.8 160 360 152.8 360 144V136z" />
|
||||
<glyph glyph-name="grin-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M200.3 200C212.7 218.7 215.4 237.3 216 256C215.5 274.7 212.7 293.3 200.3 312C192.3 324 175.2 323.4 167.6 312C155.2 293.3 152.5 274.7 151.9 256C152.4 237.3 155.2000000000001 218.7 167.6 200C175.7 188 192.8 188.6 200.3 200zM328.3 200C340.7 218.7 343.4000000000001 237.3 344 256C343.5 274.7 340.7 293.3 328.3 312C320.3 324 303.2 323.4 295.6 312C283.2000000000001 293.3 280.5 274.7 279.9000000000001 256C280.4000000000001 237.3 283.2000000000001 218.7 295.6 200C303.7000000000001 188 320.8 188.6 328.3 200zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.5 146.5 123 138.1 124.7 128.1C132.6 80.9 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4z" />
|
||||
<glyph glyph-name="grin-beam-sweat"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M440 288C469.5 288 493.3 314.3 493.3 346.7C493.3 371.7 461.6 422.2 447.1 444C443.5 449.3 436.4000000000001 449.3 432.9000000000001 444C418.4000000000001 422.2 386.7000000000001 371.7 386.7000000000001 346.7C386.7000000000001 314.3 410.5000000000001 288 440.0000000000001 288zM248 48C299.9 48 363.3 80.9 371.3 128C373 137.9 363.6 146.5 353.6 143.3C327.7000000000001 135 289.2000000000001 130.2 248.0000000000001 130.2S168.4 135 142.4 143.3C132.6 146.4 123 138 124.7 128C132.7000000000001 80.9 196.1 48 248.0000000000001 48zM378.3 216.3C381.9000000000001 217.4 384.3 220.8 384 224.6C380.7 266.7000000000001 351.8 296 328 296S275.3 266.7000000000001 272 224.6C271.7 220.9 274.1 217.4 277.7 216.3C281.2 215.2 285.1 216.8 287 220L296.5 237C304.2 250.7 315.7 258.6 328 258.6S351.8 250.7 359.5 237L369 220C371.1 216.4 375.2 215.4 378.3 216.3zM483.6 269.2000000000001C459 253.5000000000001 437.6 256.3 437.2000000000001 256.3C444.1 236.1 448.0000000000001 214.5 448.0000000000001 192C448.0000000000001 81.7 358.3000000000001 -8 248.0000000000001 -8S48 81.7 48 192S137.7 392 248 392C287.8 392 324.8 380.2 356 360.1C357.7 369.6 362.3 384.2 373.2 405.8C336.4 427.4 293.7 440 248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192C496 219 491.6 244.9 483.6 269.2zM168 258.6C180.3 258.6 191.8 250.7 199.5 237L209 220C211.1 216.3 215.2 215.3 218.3 216.3C221.9 217.4 224.3 220.8 224 224.6C220.7 266.7000000000001 191.8 296 168 296S115.3 266.7000000000001 112 224.6C111.7 220.9 114.1 217.4 117.7 216.3C121.2 215.2 125.1 216.8 127 220L136.5 237C144.2 250.8 155.7 258.6 168 258.6z" />
|
||||
<glyph glyph-name="grin-beam"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.6 146.5 123 138.1 124.7 128.1C132.6 80.9999999999999 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM117.7 216.3C121.2 215.2 125.1 216.8 127 220L136.5 237C144.2 250.7 155.7 258.6 168 258.6S191.8 250.7 199.5 237L209 220C211.1 216.3 215.2 215.3 218.3 216.3C221.9 217.4 224.3 220.8 224 224.6C220.7 266.7 191.8 296 168 296S115.3 266.7 112 224.6C111.7 220.9 114.1 217.4 117.7 216.3zM277.7000000000001 216.3C281.2000000000001 215.2 285.1 216.8 287.0000000000001 220L296.5000000000001 237C304.2000000000001 250.7 315.7000000000001 258.6 328.0000000000001 258.6S351.8000000000001 250.7 359.5000000000001 237L369.0000000000001 220C371.1000000000001 216.3 375.2000000000001 215.3 378.3000000000001 216.3C381.9000000000001 217.4 384.3000000000001 220.8 384.0000000000001 224.6C380.7000000000001 266.7 351.8000000000001 296 328.0000000000001 296S275.3000000000001 266.7 272.0000000000001 224.6C271.7000000000001 220.9 274.1000000000001 217.4 277.7000000000001 216.3z" />
|
||||
<glyph glyph-name="grin-hearts"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.6 146.5 123 138.1 124.7 128.1C132.6 80.9 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM200.8 192.3C205.3 191.1 210 193.8 211.3 198.3L230.7 268.2C236.3 288.5 223.3 309.3 201.9 312.7C183.3 315.7 165.5 302.9 160.4 284.8L158.4 277.7L151.3 279.6C133.1 284.3 113.1 275.3 106.4 257.6C98.7 237.4 110.2 215.7 130.6 210.4L200.8 192.3zM389.6 257.6C382.9000000000001 275.2 362.9000000000001 284.3 344.7000000000001 279.6L337.6 277.7L335.6 284.8C330.6 302.9 312.8 315.7 294.1 312.7C272.7000000000001 309.3 259.7000000000001 288.5 265.3 268.2L284.7 198.3C285.9 193.8 290.6 191.1 295.2 192.3L365.4 210.5C385.8 215.8 397.3 237.4 389.6 257.5999999999999zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8z" />
|
||||
<glyph glyph-name="grin-squint-tears"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M117.1 63.9C91.3 60.2 33.1 50.2 16.2 33.3C-5.7 11.4 -5.3 -24.6 17.1 -47.0000000000001S75.4 -69.8000000000001 97.4 -47.9C114.3 -31 124.3 27.2 128 53C128.8 59.4 123.4 64.8 117.1 63.9zM75.9 105.6C40.3 180 53 271.9 114.6 333.4C152.4 371.2 202.6 392 256 392C292.2 392 326.8 382.2 357.2 364.3C361 384.6 365.2 400.4 369.2 412.6C333.8 430.8 294.9 440 256 440C192.5 440 129.1 415.8 80.6 367.4C6.5 293.3 -10.7 184 28.6 93.4C40.8 97.5 56.3 101.7 75.9 105.6zM428.2 293.2C473.2 216.6 463.1 116.3 397.4 50.6C359.6 12.8 309.4 -8.0000000000001 256 -8.0000000000001C225.5 -8.0000000000001 196.2 -1.0000000000001 169.6 11.8C165.7 -7.7 161.6 -23.2 157.4 -35.4C188.8 -49.0000000000001 222.4 -56.0000000000001 256.1 -56.0000000000001C319.6 -56.0000000000001 383 -31.8000000000001 431.5 16.5999999999999C509.6 94.6999999999999 524.6 211.9999999999999 476.7 305.2C464.4 301.2 448.5 297.0999999999999 428.2 293.2zM394.9 320.1C420.7 323.8 478.9 333.8 495.8 350.7C517.6999999999999 372.6 517.3 408.6 494.9 431S436.6 453.8 414.6 431.9C397.7 415 387.7 356.8 384 331C383.2 324.6 388.6 319.2 394.9 320.1zM207.9 211.8C204.9 214.8 200.7 216 196.5 215L106 192.3C100.3 190.9 96.5 185.6 96.9 179.7C97.4 173.9 102 169.2 107.8 168.7L160.1 163.9L164.9 111.6C165.4 105.8 170.1 101.2 175.9 100.7H176.8000000000001C182.3000000000001 100.7 187.1000000000001 104.4 188.5 109.8L211.1 200.3C212.1 204.5 210.9 208.8 207.9 211.8zM247.6 236.9L338.1 259.5C343.8 260.9 347.6 266.2 347.2 272.0999999999999C346.7 277.9 342.1 282.5999999999999 336.3 283.0999999999999L284 287.9L279.2 340.2C278.7 346 274 350.6 268.2 351.1C262.6 351.2 257 347.7 255.6 342L233 251.5C232 247.4 233.2 243.1 236.2 240.1C241.2 235.1 247.5 236.9 247.6 236.9zM299.6 148.4C270.5 119.3 239.9 95.5 215.7 83C206.5 78.2 205.7 65.5 214 59.6C252.9 31.9 321 53.4 357.7 90.2000000000001S416 195 388.3 233.9C382.5 242.1 369.8 241.5 364.9000000000001 232.2C352.6 208 328.7000000000001 177.5 299.6 148.4z" />
|
||||
<glyph glyph-name="grin-squint"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.5 146.5 123 138 124.7 128.1C132.6 80.9999999999999 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM118.9 184.2C122.5 180 128.8000000000001 178.5 134.2000000000001 181.7L214.2000000000001 229.7C217.8000000000001 231.9 220.0000000000001 235.8 220.0000000000001 240S217.8000000000001 248.1 214.2000000000001 250.3L134.2000000000001 298.3C129.1000000000001 301.3 122.8000000000001 300.2000000000001 118.9000000000001 295.8C115.1000000000001 291.3 115.1000000000001 284.8 118.8000000000001 280.3L152.4000000000001 240L118.8000000000001 199.7C115.0000000000001 195.2 115.1000000000001 188.6 118.9000000000001 184.2zM361.8000000000001 181.7C367.2000000000001 178.5 373.5000000000001 180 377.1000000000001 184.2C380.9000000000001 188.7 380.9000000000001 195.2 377.2000000000001 199.7L343.6 240L377.2000000000001 280.3C381.0000000000001 284.8 380.9000000000001 291.3 377.1 295.8C373.3 300.2000000000001 366.9000000000001 301.2000000000001 361.8 298.3L281.8 250.3C278.2 248.1 276 244.2 276 240S278.2 231.9 281.8 229.7L361.8 181.7z" />
|
||||
<glyph glyph-name="grin-stars"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.6 146.5 123 138.1 124.7 128.1C132.6 80.9 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM125.7 200.9C124.7 194.7 131.1 189.9 136.7 193L168 209.3L199.3000000000001 193C204.9 189.9 211.3000000000001 194.7 210.3000000000001 200.9L204.3000000000001 235.8L229.7000000000001 260.4C234.2000000000001 264.9 231.6000000000001 272.6 225.4000000000001 273.6L190.5 278.6L175 310.2C172.1 316 164 316 161.1 310.2L145.6 278.6L110.7 273.6C104.5 272.7 101.8 265 106.4 260.4L131.8 235.8L125.7 200.9zM385.4 273.6L350.5 278.6L335 310.2C332.1 316 324 316 321.1 310.2L305.6 278.6L270.7000000000001 273.6C264.5000000000001 272.7 261.8000000000001 265 266.4000000000001 260.4L291.8 235.8L285.8 200.9C284.8 194.7 291.2 189.9 296.8 193L328.1 209.3L359.4000000000001 193C365.0000000000001 189.9 371.4000000000001 194.7 370.4000000000001 200.9L364.4000000000001 235.8L389.8 260.4C394.3 265 391.6 272.6 385.4000000000001 273.6z" />
|
||||
<glyph glyph-name="grin-tears"
|
||||
unicode=""
|
||||
horiz-adv-x="640" d=" M117.1 191.9C91.3 188.2 33.1 178.2 16.2 161.3C-5.7 139.4 -5.3 103.4 17.1 80.9999999999999S75.4 58.1999999999999 97.4 80.1C114.3 97 124.3 155.2 128 181C128.8 187.4 123.4 192.8 117.1 191.9zM623.8 161.3C606.9 178.1999999999999 548.6999999999999 188.1999999999999 522.9 191.9C516.6 192.8 511.2 187.4 512.1 181.1C515.8000000000001 155.3 525.8000000000001 97.1 542.7 80.1999999999999C564.6 58.3 600.6 58.6999999999999 623 81.0999999999999C645.3 103.3999999999999 645.7 139.3999999999999 623.8 161.2999999999999zM497.1999999999999 99.6C463.8 35.7 396.9 -8 320 -8C243.1 -8 176.2 35.7 142.8 99.6C130.3 62.2000000000001 117.6 55.7 114.5 53.1C159.1 -12.7 234.5 -56 320 -56S480.9 -12.7 525.5 53.1C522.3 55.8 509.6 62.3 497.2 99.6zM122.7 223.5C137.9 318.8 220.5 392 320 392C419.5 392 502.1 318.8 517.3 223.5C519.4 223.7 522.5 225.9 566.8 216.5C554.4 342 448.7 440 320 440S85.6 342 73.2 216.6C117.7 226 120.3 223.8 122.7 223.5zM320 48C371.9 48 435.3 80.9 443.3 128C445 137.9 435.6 146.5 425.6 143.3C399.7000000000001 135 361.2000000000001 130.2 320 130.2S240.4 135 214.4 143.3C204.6 146.4 195 138 196.7 128C204.7 80.9 268.1 48 320 48zM450.3 216.3C453.9 217.4 456.3 220.8 456 224.6C452.7 266.7000000000001 423.8 296 400 296S347.3 266.7000000000001 344 224.6C343.7 220.9 346.1 217.4 349.7 216.3C353.2 215.2 357.1 216.8 359 220L368.5 237C376.2 250.7 387.7 258.6 400 258.6S423.8 250.7 431.5 237L441 220C443.1 216.4 447.2 215.4 450.3 216.3zM240 258.6C252.3 258.6 263.8 250.7 271.5 237L281 220C283.1 216.3 287.2 215.3 290.3 216.3C293.9000000000001 217.4 296.3 220.8 296 224.6C292.7 266.7000000000001 263.8 296 240 296S187.3 266.7000000000001 184 224.6C183.7 220.9 186.1 217.4 189.7 216.3C193.2 215.2 197.1 216.8 199 220L208.5 237C216.2 250.8 227.7 258.6 240 258.6z" />
|
||||
<glyph glyph-name="grin-tongue-squint"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM312 40C312 4.4 282.9 -24.5 247.1 -24C212 -23.5 184 5.8 184 41V83.8L201.7 92.6C216.7 100.1 233.2 90.9 236.6 76.1L239.4 64C241.5 54.8 254.6 54.8 256.7 64L259.5 76.1C262.9 90.9 279.3 100.2000000000001 294.4 92.6L312.1 83.8V40zM340.2 14.7C342.4 22.8 344 31.2 344 40V83.5C358.2 95.9 368.4 111 371.3 128C373 137.9 363.6 146.5 353.6 143.3C327.7000000000001 135 289.2000000000001 130.2 248.0000000000001 130.2S168.4 135 142.4 143.3C132.5 146.4 123 138 124.7 128C127.6 111 137.8000000000001 95.9 152 83.5V40C152 31.2 153.6 22.8 155.8000000000001 14.7C91.8 48.1 48 115 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 115 404.2 48.1 340.2 14.7zM377.1 295.8C373.3 300.2000000000001 366.8 301.3 361.8 298.3L281.8 250.3C278.2 248.1 276 244.2 276 240S278.2 231.9 281.8 229.7L361.8 181.7C367.2 178.5 373.5 180 377.1 184.2C380.9 188.7 380.9 195.2 377.2 199.7L343.6 240L377.2000000000001 280.3C381.0000000000001 284.8 380.9000000000001 291.4 377.1 295.8zM214.2 250.3L134.2 298.3C129.2 301.3 122.8 300.3 118.9 295.8C115.1 291.3 115.1 284.8 118.8 280.3L152.4 240L118.8 199.7C115 195.2 115.1 188.7 118.9 184.2C122.5 180 128.8 178.5 134.2 181.7L214.2 229.7C217.8 231.9 220 235.8 220 240S217.8 248.1 214.2 250.3z" />
|
||||
<glyph glyph-name="grin-tongue-wink"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M152 268C126.3 268 96.1 251.1 92.2 225.9C91.4 220.9 93.9 215.9 98.3 213.5C102.7 211.1 108.2 211.7 112 215.1L121.5 223.6C136.3 236.8 167.7 236.8 182.5 223.6L192 215.1C194.5 212.9 200 210.4 205.7 213.5C210.1 215.9 212.6 220.9 211.8 225.9C207.9 251.1 177.7 268 152 268zM328 320C283.8 320 248 284.2 248 240S283.8 160 328 160S408 195.8 408 240S372.2 320 328 320zM328 192C301.5 192 280 213.5 280 240S301.5 288 328 288S376 266.5 376 240S354.5 192 328 192zM328 264C314.7 264 304 253.3 304 240S314.7 216 328 216S352 226.7 352 240S341.3 264 328 264zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM312 40C312 4.4 282.9 -24.5 247.1 -24C212 -23.5 184 5.8 184 41V83.8L201.7 92.6C216.7 100.1 233.2 90.9 236.6 76.1L239.4 64C241.5 54.8 254.6 54.8 256.7 64L259.5 76.1C262.9 90.9 279.3 100.2000000000001 294.4 92.6L312.1 83.8V40zM340.2 14.7C342.4 22.8 344 31.2 344 40V83.5C358.2 95.9 368.4 111 371.3 128C373 137.9 363.6 146.5 353.6 143.3C327.7000000000001 135 289.2000000000001 130.2 248.0000000000001 130.2S168.4 135 142.4 143.3C132.5 146.4 123 138 124.7 128C127.6 111 137.8000000000001 95.9 152 83.5V40C152 31.2 153.6 22.8 155.8000000000001 14.7C91.8 48.1 48 115 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 115 404.2 48.1 340.2 14.7z" />
|
||||
<glyph glyph-name="grin-tongue"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM312 40C312 4.4 282.9 -24.5 247.1 -24C212 -23.5 184 5.8 184 41V83.8L201.7 92.6C216.7 100.1 233.2 90.9 236.6 76.1L239.4 64C241.5 54.8 254.6 54.8 256.7 64L259.5 76.1C262.9 90.9 279.3 100.2000000000001 294.4 92.6L312.1 83.8V40zM340.2 14.7C342.4 22.8 344 31.2 344 40V83.5C358.2 95.9 368.4 111 371.3 128C373 137.9 363.6 146.5 353.6 143.3C327.7000000000001 135 289.2000000000001 130.2 248.0000000000001 130.2S168.4 135 142.4 143.3C132.5 146.4 123 138 124.7 128C127.6 111 137.8000000000001 95.9 152 83.5V40C152 31.2 153.6 22.8 155.8000000000001 14.7C91.8 48.1 48 115 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 115 404.2 48.1 340.2 14.7zM168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
|
||||
<glyph glyph-name="grin-wink"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M328 268C302.31 268 272.12 251.08 268.14 225.88C266.39 214.66 279.64 207.64 287.97 215.04L297.52 223.52C312.33 236.71 343.68 236.71 358.49 223.52L368.04 215.04C376.5200000000001 207.61 389.6 214.79 387.87 225.88C383.88 251.08 353.69 268 328 268zM168 208C185.67 208 200 222.33 200 240S185.67 272 168 272S136 257.67 136 240S150.33 208 168 208zM353.55 143.36C327.62 135.06 289.15 130.3 248 130.3S168.38 135.05 142.45 143.36C132.51 146.49 123.05 137.99 124.74 128.0200000000001C132.67 80.87 196.06 48 248 48S363.33 80.87 371.26 128.02C372.94 137.91 363.59 146.5 353.55 143.36zM248 440C111.03 440 0 328.9700000000001 0 192S111.03 -56 248 -56S496 55.03 496 192S384.9700000000001 440 248 440zM248 -8C137.72 -8 48 81.72 48 192S137.72 392 248 392S448 302.28 448 192S358.28 -8 248 -8z" />
|
||||
<glyph glyph-name="grin"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM353.6 143.4C327.7000000000001 135.1 289.2000000000001 130.3 248.0000000000001 130.3S168.4 135.1 142.4 143.4C132.5 146.5 123 138 124.7 128.1C132.6 80.9999999999999 196 48.1 248.0000000000001 48.1S363.3 80.9999999999999 371.3 128.1C372.9000000000001 137.9 363.6 146.5 353.6 143.4zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 208C345.7 208 360 222.3 360 240S345.7 272 328 272S296 257.7 296 240S310.3 208 328 208z" />
|
||||
<glyph glyph-name="hand-lizard"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M556.686 157.458L410.328 383.171C397.001 403.728 374.417 416 349.917 416H56C25.121 416 0 390.878 0 360V352C0 307.8880000000001 35.888 272 80 272H276.0420000000001L257.7090000000001 224H144C95.477 224 56 184.523 56 136C56 105.121 81.121 80 112 80H243.552C246.539 80 249.466 79.451 252.249 78.369L352 39.582V-32H576V92.171C576 115.396 569.321 137.972 556.686 157.458zM528 16H400V39.582C400 59.53 387.986 77.09 369.396 84.318L269.645 123.106A71.733 71.733 0 0 1 243.552 128H112C107.589 128 104 131.589 104 136C104 158.056 121.944 176 144 176H257.709C277.476 176 295.495 188.407 302.549 206.873L327.101 271.154C336.097 294.707 318.673 320 293.471 320H80C62.355 320 48 334.355 48 352V360C48 364.411 51.589 368 56 368H349.917C358.083 368 365.61 363.91 370.054 357.058L516.412 131.343A71.84 71.84 0 0 0 528 92.171V16z" />
|
||||
<glyph glyph-name="hand-paper"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M372.57 335.359V346.184C372.57 389.796 332.05 422.875 289.531 411.73C263.902 461.23 195.441 459.18 171.549 410.983C130.269 421.544 89.144 390.055 89.144 346V219.87C69.191 227.297 45.836 224.938 27.061 210.999C-2.294 189.203 -8.733 147.666 12.511 117.846L132.48 -50.569A32 32 0 0 1 158.542 -64.001H381.439C396.343 -64.001 409.274 -53.712 412.621 -39.188L442.805 91.77A203.637 203.637 0 0 1 448 137.436V269C448 309.62 412.477 340.992 372.57 335.359zM399.997 137.437C399.997 125.706 398.663 113.968 396.0320000000001 102.551L368.707 -16H166.787L51.591 145.697C37.152 165.967 66.614 188.473 80.985 168.302L108.113 130.223C117.108 117.597 137.144 123.936 137.144 139.506V346C137.144 371.645 173.715 370.81 173.715 345.309V192C173.715 183.163 180.878 176 189.715 176H196.571C205.408 176 212.571 183.163 212.571 192V381C212.571 406.663 249.142 405.81 249.142 380.309V192C249.142 183.163 256.305 176 265.142 176H271.998C280.835 176 287.998 183.163 287.998 192V346.875C287.998 372.5470000000001 324.568 371.685 324.568 346.184V192C324.568 183.163 331.731 176 340.568 176H347.425C356.262 176 363.425 183.163 363.425 192V268.309C363.425 294.551 399.995 293.949 399.995 269V137.437z" />
|
||||
<glyph glyph-name="hand-peace"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M362.146 256.024C348.4360000000001 277.673 323.385 290.04 297.14 286.365V374C297.14 414.804 264.329 448 223.999 448C183.669 448 150.859 414.804 150.859 374L160 280L141.321 358.85C126.578 397.157 83.85 415.89 46.209 400.7920000000001C8.735 385.762 -9.571 343.0370000000001 5.008 305.15L60.765 160.223C30.208 135.267 16.771 102.414 36.032 68.005L90.885 -29.994C102.625 -50.97 124.73 -64 148.575 -64H354.277C385.021 -64 411.835 -42.56 418.832 -12.203L446.259 106.7960000000001A67.801 67.801 0 0 1 447.988 121.999L448 192C448 236.956 404.737 269.343 362.146 256.024zM399.987 122C399.987 120.512 399.8180000000001 119.023 399.485 117.577L372.058 -1.424C370.08 -10.006 362.768 -16 354.276 -16H148.575C142.089 -16 136.033 -12.379 132.77 -6.551L77.916 91.449C73.359 99.59 75.297 110.117 82.424 115.937L109.071 137.701A16 16 0 0 1 113.883 155.84L49.793 322.389C37.226 355.044 84.37 373.163 96.51 341.611L156.294 186.254A16 16 0 0 1 171.227 176H182.859C191.696 176 198.859 183.163 198.859 192V374C198.859 408.375 249.14 408.43 249.14 374V192C249.14 183.163 256.303 176 265.14 176H271.996C280.833 176 287.996 183.163 287.996 192V220C287.996 245.122 324.563 245.159 324.563 220V192C324.563 183.163 331.726 176 340.563 176H347.419C356.256 176 363.419 183.163 363.419 192C363.419 217.12 399.986 217.16 399.986 192V122z" />
|
||||
<glyph glyph-name="hand-point-down"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M188.8 -64C234.416 -64 272 -26.235 272 19.2V54.847A93.148 93.148 0 0 1 294.064 62.776C316.0700000000001 60.269 339.0420000000001 66.2789999999999 356.855 78.761C409.342 79.9 448 116.159 448 178.701V200C448 260.063 408 298.512 408 327.2V329.879C412.952 335.626 416 343.415 416 351.999V416C416 433.673 403.106 448 387.2 448H156.8C140.894 448 128 433.673 128 416V352C128 343.416 131.048 335.627 136 329.88V327.201C136 320.237 129.807 312.339 112.332 297.0180000000001L112.184 296.889L112.038 296.7580000000001C102.101 287.9020000000001 91.197 278.642 78.785 270.9070000000001C48.537 252.202 0 240.514 0 195.2C0 138.272 35.286 103.2 83.2 103.2C91.226 103.2 98.689 104.014 105.6 105.376V19.2C105.6 -25.899 143.701 -64 188.8 -64zM188.8 -16C170.1 -16 153.6 0.775 153.6 19.2V177.6C136.275 177.6 118.4 151.2000000000001 83.2 151.2000000000001C56.8 151.2000000000001 48 171.8250000000001 48 195.2000000000001C48 203.9940000000001 80.712 215.6450000000001 104.1 230.1260000000001C118.675 239.2000000000001 131.325 249.6500000000001 143.975 260.9250000000001C162.349 277.0340000000001 180.608 294.761 183.571 320.0000000000001H360.3230000000001C364.087 277.2100000000001 400 245.491 400 200V178.701C400 138.177 377.803 121.577 338.675 128.1C330.6740000000001 113.488 304.6960000000001 103.949 285.05 115.175C266.825 95.81 238.669 97.388 224 110.225V19.2C224 0.225 207.775 -16 188.8 -16zM328 384C328 397.255 338.745 408 352 408S376 397.255 376 384S365.255 360 352 360S328 370.745 328 384z" />
|
||||
<glyph glyph-name="hand-point-left"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M0 227.2C0 181.584 37.765 144 83.2 144H118.847A93.148 93.148 0 0 1 126.776 121.936C124.269 99.93 130.279 76.958 142.761 59.145C143.9 6.658 180.159 -32 242.701 -32H264C324.063 -32 362.512 8 391.2 8H393.879C399.626 3.048 407.415 0 415.999 0H479.999C497.672 0 511.999 12.894 511.999 28.8V259.2C511.999 275.106 497.672 288 479.999 288H415.999C407.415 288 399.626 284.952 393.879 280H391.2C384.236 280 376.338 286.193 361.017 303.668L360.888 303.8160000000001L360.757 303.962C351.901 313.899 342.641 324.803 334.906 337.215C316.202 367.463 304.514 416 259.2 416C202.272 416 167.2 380.714 167.2 332.8C167.2 324.774 168.014 317.3110000000001 169.376 310.4H83.2C38.101 310.4 0 272.299 0 227.2zM48 227.2C48 245.9 64.775 262.4 83.2 262.4H241.6C241.6 279.725 215.2 297.6 215.2 332.8C215.2 359.2 235.825 368 259.2000000000001 368C267.9940000000001 368 279.6450000000001 335.288 294.1260000000001 311.9C303.2000000000001 297.325 313.6500000000001 284.675 324.925 272.025C341.034 253.651 358.761 235.392 384 232.429V55.677C341.21 51.913 309.491 16 264 16H242.701C202.177 16 185.577 38.197 192.1 77.325C177.488 85.326 167.949 111.304 179.175 130.95C159.81 149.175 161.388 177.331 174.225 192H83.2C64.225 192 48 208.225 48 227.2zM448 88C461.255 88 472 77.255 472 64S461.255 40 448 40S424 50.745 424 64S434.745 88 448 88z" />
|
||||
<glyph glyph-name="hand-point-right"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M428.8 310.4H342.623A115.52 115.52 0 0 1 344.799 332.8C344.799 380.714 309.727 416 252.799 416C207.485 416 195.797 367.463 177.092 337.216C169.357 324.803 160.098 313.899 151.241 303.963L151.11 303.817L150.981 303.6690000000001C135.662 286.193 127.764 280 120.8 280H118.121C112.374 284.952 104.585 288 96.001 288H32C14.327 288 0 275.106 0 259.2V28.8C0 12.894 14.327 0 32 0H96C104.584 0 112.373 3.048 118.12 8H120.799C149.487 8 187.936 -32 247.999 -32H269.298C331.8400000000001 -32 368.098 6.658 369.238 59.145C381.7200000000001 76.958 387.729 99.93 385.223 121.936A93.148 93.148 0 0 1 393.152 144H428.8C474.235 144 512 181.584 512 227.2C512 272.299 473.899 310.4 428.8 310.4zM428.8 192H337.774C350.611 177.331 352.189 149.175 332.824 130.95C344.051 111.304 334.511 85.326 319.899 77.325C326.423 38.197 309.823 16 269.299 16H248C202.509 16 170.79 51.913 128 55.676V232.429C153.239 235.393 170.966 253.651 187.075 272.025C198.35 284.675 208.8 297.3250000000001 217.874 311.9C232.355 335.288 244.006 368 252.8 368C276.175 368 296.8 359.2 296.8 332.8C296.8 297.6 270.4000000000001 279.725 270.4000000000001 262.4H428.8000000000001C447.2250000000001 262.4 464.0000000000001 245.9 464.0000000000001 227.2C464.0000000000001 208.225 447.7750000000001 192 428.8000000000001 192zM88 64C88 50.745 77.255 40 64 40S40 50.745 40 64S50.745 88 64 88S88 77.255 88 64z" />
|
||||
<glyph glyph-name="hand-point-up"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M105.6 364.8V278.623A115.52 115.52 0 0 1 83.2 280.799C35.286 280.799 0 245.727 0 188.799C0 143.485 48.537 131.797 78.784 113.092C91.197 105.357 102.101 96.098 112.037 87.241L112.183 87.11L112.331 86.981C129.807 71.662 136 63.764 136 56.8V54.121C131.048 48.374 128 40.585 128 32.001V-31.999C128 -49.672 140.894 -63.999 156.8 -63.999H387.2000000000001C403.1060000000001 -63.999 416.0000000000001 -49.672 416.0000000000001 -31.999V32.001C416.0000000000001 40.585 412.9520000000001 48.374 408.0000000000001 54.121V56.8C408.0000000000001 85.488 448.0000000000001 123.937 448.0000000000001 184V205.299C448.0000000000001 267.841 409.3420000000001 304.099 356.8550000000001 305.2390000000001C339.0420000000001 317.721 316.0700000000001 323.73 294.0640000000001 321.224A93.148 93.148 0 0 1 272 329.153V364.8C272 410.235 234.416 448 188.8 448C143.701 448 105.6 409.899 105.6 364.8zM224 364.8V273.774C238.669 286.611 266.825 288.189 285.05 268.824C304.6960000000001 280.0510000000001 330.6740000000001 270.511 338.675 255.899C377.803 262.423 400 245.823 400 205.299V184C400 138.509 364.087 106.79 360.324 64H183.571C180.607 89.239 162.349 106.966 143.975 123.075C131.325 134.35 118.675 144.8 104.1 153.874C80.712 168.355 48 180.006 48 188.8C48 212.175 56.8 232.8 83.2 232.8C118.4 232.8 136.275 206.4 153.6 206.4V364.8C153.6 383.225 170.1 400 188.8 400C207.775 400 224 383.775 224 364.8zM352 24C365.255 24 376 13.255 376 0S365.255 -24 352 -24S328 -13.255 328 0S338.745 24 352 24z" />
|
||||
<glyph glyph-name="hand-pointer"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M358.182 268.639C338.689 293.4070000000001 305.5030000000001 300.584 278.31 287.737C263.183 303.4240000000001 242.128 310.2240000000001 221.715 307.366V381C221.715 417.944 191.979 448 155.429 448S89.143 417.944 89.143 381V219.871C69.234 227.281 45.871 224.965 27.06 210.999C-2.295 189.204 -8.733 147.6660000000001 12.51 117.847L122.209 -36.154C134.632 -53.59 154.741 -64 176 -64H354.286C385.088 -64 411.86 -42.5 418.843 -12.203L446.272 106.7960000000001A67.873 67.873 0 0 1 448 122V206C448 252.844 401.375 285.273 358.182 268.639zM80.985 168.303L108.111 130.224C117.106 117.598 137.142 123.937 137.142 139.507V381C137.142 406.12 173.713 406.16 173.713 381V206C173.713 197.164 180.876 190 189.713 190H196.57C205.407 190 212.57 197.164 212.57 206V241C212.57 266.12 249.141 266.16 249.141 241V206C249.141 197.164 256.304 190 265.141 190H272C280.837 190 288 197.164 288 206V227C288 252.12 324.5710000000001 252.16 324.5710000000001 227V206C324.5710000000001 197.164 331.7340000000001 190 340.5710000000001 190H347.4280000000001C356.2650000000001 190 363.4280000000001 197.164 363.4280000000001 206C363.4280000000001 231.121 399.999 231.16 399.999 206V122C399.999 120.512 399.8300000000001 119.023 399.497 117.577L372.067 -1.424C370.089 -10.006 362.777 -16 354.2850000000001 -16H176C170.231 -16 164.737 -13.122 161.303 -8.303L51.591 145.697C37.185 165.92 66.585 188.515 80.985 168.303zM176.143 48V144C176.143 152.837 182.411 160 190.143 160H196.143C203.875 160 210.143 152.837 210.143 144V48C210.143 39.163 203.875 32 196.143 32H190.143C182.41 32 176.143 39.163 176.143 48zM251.571 48V144C251.571 152.837 257.839 160 265.5710000000001 160H271.5710000000001C279.3030000000001 160 285.5710000000001 152.837 285.5710000000001 144V48C285.5710000000001 39.163 279.3030000000001 32 271.5710000000001 32H265.5710000000001C257.839 32 251.5710000000001 39.163 251.5710000000001 48zM327 48V144C327 152.837 333.268 160 341 160H347C354.7320000000001 160 361 152.837 361 144V48C361 39.163 354.7320000000001 32 347 32H341C333.268 32 327 39.163 327 48z" />
|
||||
<glyph glyph-name="hand-rock"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M408.864 368.948C386.463 402.846 342.756 411.221 310.051 392.536C280.577 424.005 230.906 423.629 201.717 392.558C154.557 419.578 93.007 387.503 91.046 331.752C44.846 342.593 0 307.999 0 260.5710000000001V203.618C0 170.877 14.28 139.664 39.18 117.984L136.89 32.903C141.142 29.201 140 27.33 140 -1e-13C140 -17.6730000000001 154.327 -32.0000000000001 172 -32.0000000000001H424C441.673 -32.0000000000001 456 -17.6730000000001 456 -1e-13C456 23.5129999999999 454.985 30.745 459.982 42.37L502.817 142.026C508.911 156.203 512 171.198 512 186.5939999999999V301.0370000000001C512 353.876 457.686 389.699 408.8640000000001 368.948zM464 186.594A64.505 64.505 0 0 0 458.718 160.981L415.8830000000001 61.326C410.653 49.155 408.0000000000001 36.286 408.0000000000001 23.076V16H188V26.286C188 42.656 180.86 58.263 168.41 69.103L70.7 154.183C56.274 166.745 48 184.764 48 203.619V260.572C48 293.78 100 294.1090000000001 100 259.895V218.667A16 16 0 0 1 105.493 206.6L112.493 200.505A16 16 0 0 1 139 212.571V329.1430000000001C139 362.24 191 362.868 191 328.466V301.7150000000001C191 292.879 198.164 285.7150000000001 207 285.7150000000001H214C222.836 285.7150000000001 230 292.879 230 301.7150000000001V342.858C230 375.992 282 376.533 282 342.181V301.7150000000001C282 292.879 289.163 285.7150000000001 298 285.7150000000001H305C313.837 285.7150000000001 321 292.879 321 301.7150000000001V329.144C321 362.174 373 362.924 373 328.467V301.716C373 292.88 380.163 285.716 389 285.716H396C404.837 285.716 412 292.88 412 301.716C412 334.862 464 335.329 464 301.039V186.5940000000001z" />
|
||||
<glyph glyph-name="hand-scissors"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 -32L326 -31.987C331.114 -31.987 336.231 -31.404 341.203 -30.258L460.202 -2.831C490.56 4.165 512 30.98 512 61.723V267.425C512 291.27 498.97 313.376 477.995 325.115L379.996 379.968C345.587 399.2290000000001 312.733 385.7920000000001 287.778 355.235L142.85 410.992C104.963 425.5710000000001 62.238 407.265 47.208 369.791C32.11 332.149 50.843 289.421 89.15 274.679L168 256L74 265.141C33.196 265.141 0 232.33 0 192.001C0 151.671 33.196 118.86 74 118.86H161.635C157.96 92.615 170.327 67.563 191.976 53.8539999999999C178.657 11.263 211.044 -32 256 -32zM256 16.013C230.84 16.013 230.88 52.58 256 52.58C264.837 52.58 272 59.743 272 68.58V75.436C272 84.273 264.837 91.436 256 91.436H228C202.841 91.436 202.878 128.003 228 128.003H256C264.837 128.003 272 135.166 272 144.003V150.859C272 159.696 264.837 166.859 256 166.859H74C39.57 166.859 39.625 217.14 74 217.14H256C264.837 217.14 272 224.303 272 233.14V244.772A16 16 0 0 1 261.746 259.705L106.389 319.49C74.837 331.63 92.957 378.773 125.611 366.207L292.16 302.116A16.001 16.001 0 0 1 310.299 306.928L332.063 333.5750000000001C337.883 340.702 348.411 342.639 356.551 338.0830000000001L454.551 283.2290000000001C460.379 279.966 464 273.911 464 267.424V61.723C464 53.232 458.006 45.919 449.424 43.941L330.423 16.514A19.743 19.743 0 0 0 326 16.012H256z" />
|
||||
<glyph glyph-name="hand-spock"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M21.096 66.21L150.188 -55.303A32 32 0 0 1 172.12 -64.001H409.7200000000001C423.8900000000001 -64.001 436.3730000000001 -54.682 440.4000000000001 -41.097L472.215 66.216A115.955 115.955 0 0 1 477 99.189V136.028C477 140.079 477.476 144.132 478.414 148.073L510.144 281.4830000000001C520.243 323.8950000000001 487.828 364.221 444.6 364.0080000000001C440.456 388.8640000000001 422.057 411.1730000000001 394.75 418.0000000000001C358.947 426.9520000000001 322.523 405.3450000000001 313.5 369.25L296.599 264L274.924 395.99C266.638 432.06 230.621 454.562 194.62 446.286C165.004 439.4820000000001 144.482 413.897 142.738 384.991C100.101 384.16 69.283 344.428 78.667 303.147L109.707 166.639C82.513 189.154 42.423 186.631 18.225 160.917C-7.151 133.956 -5.873 91.592 21.096 66.21zM53.164 128.021L53.166 128.0219999999999C60.385 135.694 72.407 136.002 80.022 128.8349999999999L133.034 78.9409999999999C143.225 69.351 160 76.6 160 90.594V160.073C160 161.266 159.866 162.456 159.603 163.619L125.473 313.791C119.877 338.408 156.975 346.651 162.527 322.212L192.926 188.4549999999999A16 16 0 0 1 208.529 176.0009999999999H217.1330000000001C227.4090000000001 176.0009999999999 235.0270000000001 185.5679999999999 232.7270000000001 195.5839999999999L191.107 376.7369999999999C185.484 401.2059999999999 222.497 409.813 228.142 385.2449999999999L273.362 188.4169999999999A16 16 0 0 1 288.956 176H302.173A16 16 0 0 1 317.695 188.119L360.067 357.6090000000001C366.171 382.0310000000001 403.029 372.7680000000001 396.932 348.3920000000001L358.805 195.88C356.284 185.792 363.92 176 374.327 176H384.021A16 16 0 0 1 399.586 188.295L426.509 301.4C432.3300000000001 325.848 469.306 317.087 463.475 292.598L431.7200000000001 159.19A100.094 100.094 0 0 1 429 136.028V99.189C429 92.641 428.057 86.138 426.195 79.8610000000001L397.775 -16H178.465L53.978 101.164C46.349 108.344 45.984 120.393 53.164 128.021z" />
|
||||
<glyph glyph-name="handshake"
|
||||
unicode=""
|
||||
horiz-adv-x="640" d=" M519.2 320.1L471.6 367.7A56.252 56.252 0 0 1 432 384H205.2C190.4 384 176.1 378.1 165.6 367.7L118 320.1H0V64.4H64C81.6 64.4 95.8 78.6 95.9 96.1H105L189.6 19.6999999999999C220.5 -5.4000000000001 263.4 -6.0000000000001 295.2 15.8999999999999C307.7 5.0999999999999 321.2 -1e-13 336.3 -1e-13C354.5 -1e-13 371.6 7.3999999999999 385.1 23.9999999999999C407.2000000000001 15.3 433.3 21.3999999999999 449.1 40.8L475.3 73.1C480.9 79.9999999999999 484.4 87.9 486.2 96.1H544.1C544.2 78.6 558.5 64.4 576 64.4H640V320.1H519.2zM48 96.4C39.2 96.4 32 103.6 32 112.4S39.2 128.4 48 128.4S64 121.2 64 112.4C64 103.5 56.8 96.4 48 96.4zM438 103.3L411.9 71.1C409.1 67.7 404.1 67.1 400.6 69.9L376.7 89.3L346.7 52.8C340.7 45.4999999999999 331.7 47.9999999999999 328.7 50.4L291.9 81.9L276.3 62.7C262.4 45.6 237.1 43 221 56.1L123.7 144.1H96V272.2H137.9L199.6 333.8C201.6 334.6 203.3 335.3 205.3 336.1H262L223.3 300.6C193.9 273.7 192.2 228.3 218.9 199.3C233.7 183.1 280.1 158.1 320.4 194.9L328.6 202.4L436.8 114.6C440.2 111.8 440.7 106.7 438 103.3zM544 144.1H474.8C472.5 146.9 469.9 149.5 467.1 151.8L364.4000000000001 235.2L376.9000000000001 246.6C383.4000000000001 252.6 383.9000000000001 262.7 377.9000000000001 269.2L367 280.9C361 287.4 350.9 287.8 344.4 281.9L289.2 231.3C279.7 222.6 263.5 221.9 254.6 231.3C245.3 241.2 246.1 256.4 255.8 265.2000000000001L321.4 325.3C328.8 332.1 338.4 335.8 348.4 335.8L432.1 336C434.2 336 436.2 335.2000000000001 437.6 333.7000000000001L499.3 272.1H544V144.1zM592 96.4C583.2 96.4 576 103.6 576 112.4S583.2 128.4 592 128.4S608 121.2 608 112.4C608 103.5 600.8 96.4 592 96.4z" />
|
||||
<glyph glyph-name="hdd"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M567.403 212.358L462.323 363.411A48 48 0 0 1 422.919 384H153.081A48 48 0 0 1 113.677 363.411L8.597 212.358A48.001 48.001 0 0 1 0 184.946V48C0 21.49 21.49 0 48 0H528C554.51 0 576 21.49 576 48V184.946C576 194.747 573 204.312 567.403 212.358zM153.081 336H422.919L500.832 224H75.168L153.081 336zM528 48H48V176H528V48zM496 112C496 94.327 481.673 80 464 80S432 94.327 432 112S446.327 144 464 144S496 129.673 496 112zM400 112C400 94.327 385.673 80 368 80S336 94.327 336 112S350.327 144 368 144S400 129.673 400 112z" />
|
||||
<glyph glyph-name="heart"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M458.4 383.7C400.6 432.3 311.3 425 256 368.7C200.7 425 111.4 432.4 53.6 383.7C-21.6 320.4 -10.6 217.2 43 162.5L218.4 -16.2C228.4 -26.4 241.8 -32.1 256 -32.1C270.3 -32.1 283.6 -26.4999999999999 293.6 -16.3L469 162.4C522.5 217.1 533.7 320.3 458.4 383.7zM434.8 196.2L259.4 17.5C257 15.1 255 15.1 252.6 17.5L77.2 196.2C40.7 233.4 33.3 303.8 84.5 346.9C123.4 379.6 183.4 374.7 221 336.4L256 300.7L291 336.4C328.8 374.9 388.8 379.6 427.5 347C478.6 303.9 471 233.1 434.8 196.2z" />
|
||||
<glyph glyph-name="hospital"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M128 204V244C128 250.627 133.373 256 140 256H180C186.627 256 192 250.627 192 244V204C192 197.373 186.627 192 180 192H140C133.373 192 128 197.373 128 204zM268 192H308C314.627 192 320 197.373 320 204V244C320 250.627 314.627 256 308 256H268C261.373 256 256 250.627 256 244V204C256 197.373 261.373 192 268 192zM192 108V148C192 154.627 186.627 160 180 160H140C133.373 160 128 154.627 128 148V108C128 101.373 133.373 96 140 96H180C186.627 96 192 101.373 192 108zM268 96H308C314.627 96 320 101.373 320 108V148C320 154.627 314.627 160 308 160H268C261.373 160 256 154.627 256 148V108C256 101.373 261.373 96 268 96zM448 -28V-64H0V-28C0 -21.373 5.373 -16 12 -16H31.5V362.9650000000001C31.5 374.582 42.245 384 55.5 384H144V424C144 437.255 154.745 448 168 448H280C293.255 448 304 437.255 304 424V384H392.5C405.755 384 416.5 374.582 416.5 362.9650000000001V-16H436C442.627 -16 448 -21.373 448 -28zM79.5 -15H192V52C192 58.627 197.373 64 204 64H244C250.627 64 256 58.627 256 52V-15H368.5V336H304V312C304 298.745 293.255 288 280 288H168C154.745 288 144 298.745 144 312V336H79.5V-15zM266 384H240V410A6 6 0 0 1 234 416H214A6 6 0 0 1 208 410V384H182A6 6 0 0 1 176 378V358A6 6 0 0 1 182 352H208V326A6 6 0 0 1 214 320H234A6 6 0 0 1 240 326V352H266A6 6 0 0 1 272 358V378A6 6 0 0 1 266 384z" />
|
||||
<glyph glyph-name="hourglass"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M368 400H372C378.627 400 384 405.373 384 412V436C384 442.627 378.627 448 372 448H12C5.373 448 0 442.627 0 436V412C0 405.373 5.373 400 12 400H16C16 319.4360000000001 48.188 234.193 113.18 192C47.899 149.619 16 64.1 16 -16H12C5.373 -16 0 -21.373 0 -28V-52C0 -58.627 5.373 -64 12 -64H372C378.627 -64 384 -58.627 384 -52V-28C384 -21.373 378.627 -16 372 -16H368C368 64.564 335.812 149.807 270.82 192C336.102 234.381 368 319.9 368 400zM64 400H320C320 298.38 262.693 216 192 216S64 298.379 64 400zM320 -16H64C64 85.62 121.308 168 192 168S320 85.62 320 -16z" />
|
||||
<glyph glyph-name="id-badge"
|
||||
unicode=""
|
||||
horiz-adv-x="384" d=" M336 448H48C21.5 448 0 426.5 0 400V-16C0 -42.5 21.5 -64 48 -64H336C362.5 -64 384 -42.5 384 -16V400C384 426.5 362.5 448 336 448zM336 -16H48V400H336V-16zM144 336H240C248.8 336 256 343.2 256 352S248.8 368 240 368H144C135.2 368 128 360.8 128 352S135.2 336 144 336zM192 160C227.3 160 256 188.7 256 224S227.3 288 192 288S128 259.3 128 224S156.7 160 192 160zM102.4 32H281.6C294 32 304 40.6 304 51.2V70.4C304 102.2 273.9 128 236.8 128C226 128 218.1 120 192 120C165.1 120 158.6 128 147.2 128C110.1 128 80 102.2 80 70.4V51.2C80 40.6 90 32 102.4 32z" />
|
||||
<glyph glyph-name="id-card"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M528 416H48C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48 -32H528C554.5 -32 576 -10.5 576 16V368C576 394.5 554.5 416 528 416zM528 16H303.2C304.1 20.5 304 12.4 304 38.4C304 70.2 273.9 96 236.8 96C226 96 218.1 88 192 88C165.1 88 158.6 96 147.2 96C110.1 96 80 70.2 80 38.4C80 12.4 79.8 20.5 80.8 16H48V304H528V16zM360 96H472C476.4 96 480 99.6 480 104V120C480 124.4 476.4 128 472 128H360C355.6 128 352 124.4 352 120V104C352 99.6 355.6 96 360 96zM360 160H472C476.4 160 480 163.6 480 168V184C480 188.4 476.4 192 472 192H360C355.6 192 352 188.4 352 184V168C352 163.6 355.6 160 360 160zM360 224H472C476.4 224 480 227.6 480 232V248C480 252.4 476.4 256 472 256H360C355.6 256 352 252.4 352 248V232C352 227.6 355.6 224 360 224zM192 128C227.3 128 256 156.7 256 192S227.3 256 192 256S128 227.3 128 192S156.7 128 192 128z" />
|
||||
<glyph glyph-name="image"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 384H48C21.49 384 0 362.51 0 336V48C0 21.49 21.49 0 48 0H464C490.51 0 512 21.49 512 48V336C512 362.51 490.51 384 464 384zM458 48H54A6 6 0 0 0 48 54V330A6 6 0 0 0 54 336H458A6 6 0 0 0 464 330V54A6 6 0 0 0 458 48zM128 296C105.909 296 88 278.091 88 256S105.909 216 128 216S168 233.909 168 256S150.091 296 128 296zM96 96H416V176L328.485 263.515C323.7990000000001 268.201 316.201 268.201 311.514 263.515L192 144L152.485 183.515C147.799 188.201 140.201 188.201 135.514 183.515L96 144V96z" />
|
||||
<glyph glyph-name="images"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M480 32V16C480 -10.51 458.51 -32 432 -32H48C21.49 -32 0 -10.51 0 16V272C0 298.51 21.49 320 48 320H64V272H54A6 6 0 0 1 48 266V22A6 6 0 0 1 54 16H426A6 6 0 0 1 432 22V32H480zM522 368H150A6 6 0 0 1 144 362V118A6 6 0 0 1 150 112H522A6 6 0 0 1 528 118V362A6 6 0 0 1 522 368zM528 416C554.51 416 576 394.51 576 368V112C576 85.49 554.51 64 528 64H144C117.49 64 96 85.49 96 112V368C96 394.51 117.49 416 144 416H528zM264 304C264 281.909 246.091 264 224 264S184 281.909 184 304S201.909 344 224 344S264 326.091 264 304zM192 208L231.515 247.515C236.201 252.201 243.799 252.201 248.486 247.515L288 208L391.515 311.515C396.201 316.201 403.799 316.201 408.486 311.515L480 240V160H192V208z" />
|
||||
<glyph glyph-name="keyboard"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M528 384H48C21.49 384 0 362.51 0 336V48C0 21.49 21.49 0 48 0H528C554.51 0 576 21.49 576 48V336C576 362.51 554.51 384 528 384zM536 48C536 43.589 532.411 40 528 40H48C43.589 40 40 43.589 40 48V336C40 340.411 43.589 344 48 344H528C532.411 344 536 340.411 536 336V48zM170 178V206C170 212.627 164.627 218 158 218H130C123.373 218 118 212.627 118 206V178C118 171.373 123.373 166 130 166H158C164.627 166 170 171.373 170 178zM266 178V206C266 212.627 260.627 218 254 218H226C219.373 218 214 212.627 214 206V178C214 171.373 219.373 166 226 166H254C260.627 166 266 171.373 266 178zM362 178V206C362 212.627 356.627 218 350 218H322C315.373 218 310 212.627 310 206V178C310 171.373 315.373 166 322 166H350C356.627 166 362 171.373 362 178zM458 178V206C458 212.627 452.627 218 446 218H418C411.373 218 406 212.627 406 206V178C406 171.373 411.373 166 418 166H446C452.627 166 458 171.373 458 178zM122 96V124C122 130.627 116.627 136 110 136H82C75.373 136 70 130.627 70 124V96C70 89.373 75.373 84 82 84H110C116.627 84 122 89.373 122 96zM506 96V124C506 130.627 500.627 136 494 136H466C459.373 136 454 130.627 454 124V96C454 89.373 459.373 84 466 84H494C500.627 84 506 89.373 506 96zM122 260V288C122 294.627 116.627 300 110 300H82C75.373 300 70 294.627 70 288V260C70 253.373 75.373 248 82 248H110C116.627 248 122 253.373 122 260zM218 260V288C218 294.627 212.627 300 206 300H178C171.373 300 166 294.627 166 288V260C166 253.373 171.373 248 178 248H206C212.627 248 218 253.373 218 260zM314 260V288C314 294.627 308.627 300 302 300H274C267.373 300 262 294.627 262 288V260C262 253.373 267.373 248 274 248H302C308.627 248 314 253.373 314 260zM410 260V288C410 294.627 404.627 300 398 300H370C363.373 300 358 294.627 358 288V260C358 253.373 363.373 248 370 248H398C404.627 248 410 253.373 410 260zM506 260V288C506 294.627 500.627 300 494 300H466C459.373 300 454 294.627 454 288V260C454 253.373 459.373 248 466 248H494C500.627 248 506 253.373 506 260zM408 102V118C408 124.627 402.627 130 396 130H180C173.373 130 168 124.627 168 118V102C168 95.373 173.373 90 180 90H396C402.627 90 408 95.373 408 102z" />
|
||||
<glyph glyph-name="kiss-beam"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M168 296C144.2 296 115.3 266.7 112 224.6C111.7 220.9 114 217.4 117.6 216.3C121.1 215.3 125.1 216.8 126.9 220L136.4 237C144.1 250.7 155.6 258.6 167.9 258.6S191.7 250.7 199.4 237L208.9 220C211 216.3 215.1 215.3 218.2 216.3C221.8 217.4 224.1 220.8 223.8 224.6C220.7 266.7 191.8 296 168 296zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM304 140C304 159.2 275.2 181.5 232.5 184C228.7 184.4 225.1 181.6 224.3 177.8C223.4 174 225.4 170.1 229 168.6L245.9 161.4C258.9 155.9 266.7 147.9 266.7 139.9S258.9 123.9 246 118.4L229 111.2000000000001C223.3 108.8000000000001 223 99.0000000000001 229 96.4L245.9 89.2000000000001C258.9 83.7000000000001 266.7 75.7000000000001 266.7 67.7000000000001S258.9 51.7 246 46.2L229 39.0000000000001C225.4 37.5000000000001 223.4 33.6000000000001 224.3 29.8000000000001C225.1 26.2 228.4 23.6000000000001 232.1 23.6000000000001H232.6C275.4000000000001 26.1000000000001 304.1 48.4000000000001 304.1 67.6000000000001C304.1 80.6000000000001 290.7000000000001 94.9000000000001 268.9000000000001 103.6000000000001C290.6 112.7 304 127 304 140zM328 296C304.2 296 275.3 266.7 272 224.6C271.7 220.9 274 217.4 277.6 216.3C281.1 215.3 285.1 216.8 286.9000000000001 220L296.4000000000001 237C304.1 250.7 315.6 258.6 327.9000000000001 258.6S351.7000000000001 250.7 359.4000000000001 237L368.9000000000001 220C371.0000000000001 216.3 375.1 215.3 378.2000000000001 216.3C381.8000000000001 217.4 384.1 220.8 383.8000000000001 224.6C380.7000000000001 266.7 351.8000000000001 296 328.0000000000001 296z" />
|
||||
<glyph glyph-name="kiss-wink-heart"
|
||||
unicode=""
|
||||
horiz-adv-x="504" d=" M304 139.5C304 158.7 275.2 181 232.5 183.5C228.7 183.9 225.1 181.1 224.3 177.3C223.4 173.5 225.4 169.6 229 168.1L245.9 160.9C258.9 155.4 266.7 147.4 266.7 139.4S258.9 123.4 246 117.9L229 110.7000000000001C223.3 108.3000000000001 223 98.5000000000001 229 95.9L245.9 88.7000000000001C258.9 83.2000000000001 266.7 75.2000000000001 266.7 67.2000000000001S258.9 51.2 246 45.7L229 38.5000000000001C225.4 37.0000000000001 223.4 33.1000000000001 224.3 29.3000000000001C225.1 25.7 228.4 23.1000000000001 232.1 23.1000000000001H232.6C275.4000000000001 25.6000000000001 304.1 47.9000000000001 304.1 67.1000000000001C304.1 80.1000000000001 290.7000000000001 94.4000000000001 268.9000000000001 103.1000000000001C290.6 112.2000000000001 304.0000000000001 126.5000000000001 304.0000000000001 139.5000000000001zM374.5 223L384 214.5C387.8 211.2 393.3 210.5 397.7 212.9C402.1 215.3 404.6 220.3 403.8 225.3C399.8 250.5 369.6 267.4 344 267.4S288.1 250.5 284.2 225.3C283.4 220.3 285.9 215.3 290.3 212.9C296.1 209.8 301.5 212.2 304 214.5L313.5 223C328.3 236.2 359.7 236.2 374.5 223zM136 239.5C136 221.8 150.3 207.5 168 207.5S200 221.8 200 239.5S185.7 271.5 168 271.5S136 257.2 136 239.5zM501.1 45.5C493.1 66.3 469.6 77 448 71.4L439.6 69.2L437.3 77.6C431.4000000000001 98.9999999999999 410.3 114.1 388.3 110.6C363.1 106.6 347.7 81.9999999999999 354.3 57.9999999999999L377.2 -24.6C378.7 -29.9 384.2 -33.1 389.6 -31.7L472.6 -10.2C496.7 -3.9 510.3 21.6 501.1 45.4999999999999zM334 11.7C307.9 -0.8 278.8 -8 248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192C448 169.9 444.3 148.7 437.6 128.8C446.6 122.4 454.6 114.6 460.2 104.9C466.6 104.8 472.8000000000001 103.5000000000001 478.8000000000001 102.0000000000001C489.7 129.9 495.9000000000001 160.2000000000001 495.9000000000001 192.0000000000001C496 329 385 440 248 440S0 329 0 192S111 -56 248 -56C283.4 -56 316.9 -48.5 347.4 -35.1C344.9 -27.8 351.7 -52.3 334 11.7z" />
|
||||
<glyph glyph-name="kiss"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM304 140C304 159.2 275.2 181.5 232.5 184C228.7 184.4 225.1 181.6 224.3 177.8C223.4 174 225.4 170.1 229 168.6L245.9 161.4C258.9 155.9 266.7 147.9 266.7 139.9S258.9 123.9 246 118.4L229 111.2000000000001C223.3 108.8000000000001 223 99.0000000000001 229 96.4L245.9 89.2000000000001C258.9 83.7000000000001 266.7 75.7000000000001 266.7 67.7000000000001S258.9 51.7 246 46.2L229 39.0000000000001C225.4 37.5000000000001 223.4 33.6000000000001 224.3 29.8000000000001C225.1 26.2 228.4 23.6000000000001 232.1 23.6000000000001H232.6C275.4000000000001 26.1000000000001 304.1 48.4000000000001 304.1 67.6000000000001C304.1 80.6000000000001 290.7000000000001 94.9000000000001 268.9000000000001 103.6000000000001C290.6 112.7 304 127 304 140zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
|
||||
<glyph glyph-name="laugh-beam"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM389.4 50.6C351.6 12.8 301.4 -8 248 -8S144.4 12.8 106.6 50.6S48 138.6 48 192S68.8 295.6 106.6 333.4S194.6 392 248 392S351.6 371.2 389.4 333.4S448 245.4 448 192S427.2 88.4 389.4 50.6zM328 296C304.2 296 275.3 266.7 272 224.6C271.3 216 282.8 212.7 286.9 220.1L296.4 237.1C304.1 250.8 315.6 258.7 327.9 258.7S351.7 250.8 359.4 237.1L368.9 220.1C373 212.7 384.5 216.1 383.8 224.6C380.7 266.7 351.8 296 328 296zM127 220.1L136.5 237.1C144.2 250.8 155.7 258.7 168 258.7S191.8 250.8 199.5 237.1L209 220.1C213.1 212.7 224.6 216.1 223.9 224.6C220.6 266.7 191.7 296 167.9 296S115.2 266.7 111.9 224.6C111.3 216.1 122.8 212.7 127 220.1zM362.4 160H133.6C125.4 160 119.1 153 120.1 145C127.6 85.8 179 40 241.2 40H254.8C317 40 368.4 85.8 375.9 145C376.9 153 370.6 160 362.4 160z" />
|
||||
<glyph glyph-name="laugh-squint"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM389.4 50.6C351.6 12.8 301.4 -8 248 -8S144.4 12.8 106.6 50.6S48 138.6 48 192S68.8 295.6 106.6 333.4S194.6 392 248 392S351.6 371.2 389.4 333.4S448 245.4 448 192S427.2 88.4 389.4 50.6zM343.6 252L377.2000000000001 292.3C385.8000000000001 302.6 373.4000000000001 317.1 361.8000000000001 310.3L281.8000000000001 262.3C274.0000000000001 257.6 274.0000000000001 246.4 281.8000000000001 241.7L361.8000000000001 193.7C373.3000000000001 186.9 385.8000000000001 201.3 377.2000000000001 211.7L343.6 252zM134.2 193.7L214.2 241.7C222 246.4 222 257.6 214.2 262.3L134.2 310.3C122.6 317.2 110.2 302.6 118.8 292.3L152.4 252L118.8 211.7C110.1 201.3 122.6 186.9 134.2 193.7zM362.4 160H133.6C125.4 160 119.1 153 120.1 145C127.6 85.8 179 40 241.2 40H254.8C317 40 368.4 85.8 375.9 145C376.9 153 370.6 160 362.4 160z" />
|
||||
<glyph glyph-name="laugh-wink"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM389.4 50.6C351.6 12.8 301.4 -8 248 -8S144.4 12.8 106.6 50.6C68.8 88.4 48 138.6 48 192S68.8 295.6 106.6 333.4C144.4 371.2 194.6 392 248 392S351.6 371.2 389.4 333.4C427.2 295.6 448 245.4 448 192S427.2 88.4 389.4 50.6zM328 284C302.3 284 272.1 267.1 268.1 241.9C266.4000000000001 230.7 279.6 223.7 287.9000000000001 231.1L297.4000000000001 239.6C312.2000000000001 252.8 343.6 252.8 358.4000000000001 239.6L367.9000000000001 231.1C376.4000000000001 223.7 389.5000000000001 230.8 387.7000000000001 241.9C383.9000000000001 267.1 353.7000000000001 284 328.0000000000001 284zM168 224C185.7 224 200 238.3 200 256S185.7 288 168 288S136 273.7 136 256S150.3 224 168 224zM362.4 160H133.6C125.4 160 119.1 153 120.1 145C127.6 85.8 179 40 241.2 40H254.8C317 40 368.4 85.8 375.9 145C376.9 153 370.6 160 362.4 160z" />
|
||||
<glyph glyph-name="laugh"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM389.4 50.6C351.6 12.8 301.4 -8 248 -8S144.4 12.8 106.6 50.6S48 138.6 48 192S68.8 295.6 106.6 333.4S194.6 392 248 392S351.6 371.2 389.4 333.4S448 245.4 448 192S427.2 88.4 389.4 50.6zM328 224C345.7 224 360 238.3 360 256S345.7 288 328 288S296 273.7 296 256S310.3 224 328 224zM168 224C185.7 224 200 238.3 200 256S185.7 288 168 288S136 273.7 136 256S150.3 224 168 224zM362.4 160H133.6C125.4 160 119.1 153 120.1 145C127.6 85.8 179 40 241.2 40H254.8C317 40 368.4 85.8 375.9 145C376.9 153 370.6 160 362.4 160z" />
|
||||
<glyph glyph-name="lemon"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M484.112 420.111C455.989 448.233 416.108 456.057 387.0590000000001 439.135C347.604 416.152 223.504 489.111 91.196 356.803C-41.277 224.328 31.923 100.528 8.866 60.942C-8.056 31.891 -0.234 -7.99 27.888 -36.112C56.023 -64.247 95.899 -72.0499999999999 124.945 -55.133C164.368 -32.163 288.502 -105.102 420.803 27.196C553.277 159.673 480.076 283.473 503.134 323.057C520.056 352.1070000000001 512.234 391.988 484.112 420.111zM461.707 347.217C422.907 280.608 507.307 181.582 386.862 61.137C266.422 -59.306 167.387 25.089 100.786 -13.706C78.1069999999999 -26.913 36.751 13.535 50.2929999999999 36.782C89.0929999999999 103.391 4.6929999999999 202.417 125.138 322.862C245.573 443.298 344.616 358.914 411.219 397.708C433.949 410.948 475.224 370.42 461.707 347.217zM291.846 338.481C293.216 327.521 285.442 317.524 274.481 316.154C219.635 309.299 138.702 228.367 131.846 173.519C130.473 162.53 120.447 154.785 109.52 156.154C98.559 157.524 90.785 167.52 92.155 178.48C101.317 251.766 196.322 346.6950000000001 269.5200000000001 355.8450000000001C280.473 357.213 290.4760000000001 349.442 291.8460000000001 338.481z" />
|
||||
<glyph glyph-name="life-ring"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 -56C392.967 -56 504 55.033 504 192S392.967 440 256 440S8 328.967 8 192S119.033 -56 256 -56zM152.602 20.72L206.013 74.131C237.819 60.625 274.141 60.609 305.987 74.131L359.398 20.72C296.1810000000001 -17.599 215.819 -17.599 152.602 20.72zM336 192C336 147.888 300.112 112 256 112S176 147.888 176 192S211.888 272 256 272S336 236.112 336 192zM427.28 88.602L373.869 142.013C387.374 173.819 387.391 210.141 373.869 241.987L427.28 295.398C465.599 232.181 465.599 151.819 427.28 88.602zM359.397 363.28L305.986 309.8690000000001C274.18 323.374 237.858 323.391 206.013 309.8690000000001L152.602 363.28C215.819 401.599 296.1810000000001 401.599 359.397 363.28zM84.72 295.398L138.131 241.987C124.625 210.181 124.609 173.859 138.131 142.013L84.72 88.602C46.401 151.819 46.401 232.181 84.72 295.398z" />
|
||||
<glyph glyph-name="lightbulb"
|
||||
unicode=""
|
||||
horiz-adv-x="352" d=" M176 368C123.06 368 80 324.94 80 272C80 263.16 87.16 256 96 256S112 263.16 112 272C112 307.3 140.72 336 176 336C184.84 336 192 343.16 192 352S184.84 368 176 368zM96.06 -11.17C96.06 -14.32 96.99 -17.39 98.74 -20.01L123.25 -56.85C126.22 -61.31 131.22 -63.99 136.57 -63.99H215.42C220.78 -63.99 225.7800000000001 -61.31 228.74 -56.85L253.25 -20.01C254.99 -17.39 255.92 -14.31 255.93 -11.17L255.98 32.01H96.02L96.06 -11.17zM176 448C73.72 448 0 365.03 0 272C0 227.63 16.45 187.15 43.56 156.22C60.2 137.23 86.3 97.42 95.98 64.0600000000001V64.0000000000001H143.98V64.1200000000001C143.97 68.89 143.26 73.6300000000001 141.83 78.1900000000001C136.24 96.0000000000001 119.01 142.96 79.66 187.8600000000001C59.12 211.2900000000001 48.14 241.0100000000001 48.05 272.0000000000001C47.85 345.6400000000001 107.72 400.0000000000001 176 400.0000000000001C246.58 400.0000000000001 304 342.5800000000001 304 272.0000000000001C304 241.0300000000001 292.76 211.1500000000001 272.35 187.8600000000001C233.24 143.2500000000001 215.93 96.3900000000001 210.25 78.4000000000001A47.507 47.507 0 0 1 208.03 64.1000000000001V64.0000000000001H256.0300000000001V64.0500000000001C265.7100000000001 97.4200000000001 291.8100000000001 137.2300000000001 308.4500000000001 156.21C335.55 187.15 352 227.63 352 272C352 369.2 273.2 448 176 448z" />
|
||||
<glyph glyph-name="list-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 416H48C21.49 416 0 394.51 0 368V16C0 -10.51 21.49 -32 48 -32H464C490.51 -32 512 -10.51 512 16V368C512 394.51 490.51 416 464 416zM458 16H54A6 6 0 0 0 48 22V362A6 6 0 0 0 54 368H458A6 6 0 0 0 464 362V22A6 6 0 0 0 458 16zM416 108V84C416 77.373 410.627 72 404 72H204C197.373 72 192 77.373 192 84V108C192 114.627 197.373 120 204 120H404C410.627 120 416 114.627 416 108zM416 204V180C416 173.373 410.627 168 404 168H204C197.373 168 192 173.373 192 180V204C192 210.627 197.373 216 204 216H404C410.627 216 416 210.627 416 204zM416 300V276C416 269.373 410.627 264 404 264H204C197.373 264 192 269.373 192 276V300C192 306.627 197.373 312 204 312H404C410.627 312 416 306.627 416 300zM164 288C164 268.118 147.882 252 128 252S92 268.118 92 288S108.118 324 128 324S164 307.882 164 288zM164 192C164 172.118 147.882 156 128 156S92 172.118 92 192S108.118 228 128 228S164 211.882 164 192zM164 96C164 76.118 147.882 60 128 60S92 76.118 92 96S108.118 132 128 132S164 115.882 164 96z" />
|
||||
<glyph glyph-name="map"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M560.02 416C558.06 416 556.04 415.63 554.06 414.8400000000001L384.01 352H384L212 412.7200000000001A64.252 64.252 0 0 1 191.76 416C185.07 416 178.39 414.95 171.95 412.86L20.12 360.05A32.006 32.006 0 0 1 0 330.3400000000001V-15.98C0 -25.17 7.53 -32 15.99 -32C17.95 -32 19.96 -31.63 21.95 -30.84L192 32L364 -28.71A63.97999999999999 63.97999999999999 0 0 1 404.05 -28.86L555.88 23.95A31.996 31.996 0 0 1 576 53.66V399.98C576 409.17 568.47 416 560.02 416zM224 357.58L352 312.39V26.42L224 71.61V357.58zM48 29.95V318.93L176 363.4600000000001V77.26L175.36 77.03L48 29.95zM528 65.08L400 20.55V306.74L400.64 306.98L528 354.05V65.08z" />
|
||||
<glyph glyph-name="meh-blank"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM168 272C150.3 272 136 257.7 136 240S150.3 208 168 208S200 222.3 200 240S185.7 272 168 272zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
|
||||
<glyph glyph-name="meh-rolling-eyes"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM336 296C296.2 296 264 263.8 264 224S296.2 152 336 152S408 184.2 408 224S375.8 296 336 296zM336 184C313.9 184 296 201.9 296 224C296 237.6 303.3 249.1 313.7 256.3C312.7 253.7 312 251 312 248C312 234.7 322.7 224 336 224S360 234.7 360 248C360 250.9 359.3 253.7 358.3 256.3C368.7 249.1 376 237.6 376 224C376 201.9 358.1 184 336 184zM232 224C232 263.8 199.8 296 160 296S88 263.8 88 224S120.2 152 160 152S232 184.2 232 224zM120 224C120 237.6 127.3 249.1 137.7 256.3C136.7 253.7 136 251 136 248C136 234.7 146.7 224 160 224S184 234.7 184 248C184 250.9 183.3 253.7 182.3 256.3C192.7 249.1 200 237.6 200 224C200 201.9 182.1 184 160 184S120 201.9 120 224zM312 96H184C170.8 96 160 85.2 160 72S170.8 48 184 48H312C325.2 48 336 58.8 336 72S325.2 96 312 96z" />
|
||||
<glyph glyph-name="meh"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272zM336 128H160C146.8 128 136 117.2 136 104S146.8 80 160 80H336C349.2 80 360 90.8 360 104S349.2 128 336 128z" />
|
||||
<glyph glyph-name="minus-square"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M108 164C101.4 164 96 169.4 96 176V208C96 214.6 101.4 220 108 220H340C346.6 220 352 214.6 352 208V176C352 169.4 346.6 164 340 164H108zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="money-bill-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="640" d=" M320 304C266.98 304 224 253.86 224 192C224 130.15 266.98 80 320 80C373 80 416 130.13 416 192C416 253.86 373.02 304 320 304zM360 136C360 131.58 356.42 128 352 128H288C283.58 128 280 131.58 280 136V152C280 156.42 283.58 160 288 160H304V215.44L303.53 215.13A7.991999999999999 7.991999999999999 0 0 0 292.44 217.35L283.56 230.66A7.991999999999999 7.991999999999999 0 0 0 285.7800000000001 241.75L301.11 251.97A23.99 23.99 0 0 0 314.42 256H328C332.42 256 336 252.42 336 248V160H352C356.42 160 360 156.42 360 152V136zM608 384H32C14.33 384 0 369.67 0 352V32C0 14.33 14.33 0 32 0H608C625.67 0 640 14.33 640 32V352C640 369.67 625.67 384 608 384zM592 112C556.65 112 528 83.35 528 48H112C112 83.35 83.35 112 48 112V272C83.35 272 112 300.65 112 336H528C528 300.65 556.65 272 592 272V112z" />
|
||||
<glyph glyph-name="moon"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M279.135 -64C357.891 -64 430.117 -28.196 477.979 30.775C506.249 65.606 475.421 116.497 431.73 108.176C349.382 92.493 273.458 155.444 273.458 238.968C273.458 287.392 299.518 331.26 340.892 354.804C379.637 376.854 369.891 435.592 325.87 443.723A257.936 257.936 0 0 1 279.135 448C137.775 448 23.135 333.425 23.135 192C23.135 50.64 137.711 -64 279.135 -64zM279.135 400C292.12 400 304.824 398.799 317.151 396.522C262.391 365.359 225.4580000000001 306.48 225.4580000000001 238.968C225.4580000000001 125.12 329.0990000000001 39.768 440.7100000000001 61.024C402.574 14.036 344.366 -16 279.135 -16C164.26 -16 71.135 77.125 71.135 192S164.26 400 279.135 400z" />
|
||||
<glyph glyph-name="newspaper"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M552 384H112C91.142 384 73.357 370.623 66.752 352H24C10.745 352 0 341.255 0 328V56C0 25.072 25.072 0 56 0H552C565.255 0 576 10.745 576 24V360C576 373.255 565.255 384 552 384zM48 56V304H64V56C64 51.589 60.411 48 56 48S48 51.589 48 56zM528 48H111.422C111.796 50.614 112 53.283 112 56V336H528V48zM172 168H308C314.627 168 320 173.373 320 180V276C320 282.627 314.627 288 308 288H172C165.373 288 160 282.627 160 276V180C160 173.373 165.373 168 172 168zM200 248H280V208H200V248zM160 108V132C160 138.627 165.373 144 172 144H308C314.627 144 320 138.627 320 132V108C320 101.373 314.627 96 308 96H172C165.373 96 160 101.373 160 108zM352 108V132C352 138.627 357.373 144 364 144H468C474.627 144 480 138.627 480 132V108C480 101.373 474.627 96 468 96H364C357.373 96 352 101.373 352 108zM352 252V276C352 282.627 357.373 288 364 288H468C474.627 288 480 282.627 480 276V252C480 245.373 474.627 240 468 240H364C357.373 240 352 245.373 352 252zM352 180V204C352 210.627 357.373 216 364 216H468C474.627 216 480 210.627 480 204V180C480 173.373 474.627 168 468 168H364C357.373 168 352 173.373 352 180z" />
|
||||
<glyph glyph-name="object-group"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M500 320C506.627 320 512 325.373 512 332V404C512 410.627 506.627 416 500 416H428C421.373 416 416 410.627 416 404V392H96V404C96 410.627 90.627 416 84 416H12C5.373 416 0 410.627 0 404V332C0 325.373 5.373 320 12 320H24V64H12C5.373 64 0 58.627 0 52V-20C0 -26.627 5.373 -32 12 -32H84C90.627 -32 96 -26.627 96 -20V-8H416V-20C416 -26.627 421.373 -32 428 -32H500C506.627 -32 512 -26.627 512 -20V52C512 58.627 506.627 64 500 64H488V320H500zM448 384H480V352H448V384zM32 384H64V352H32V384zM64 0H32V32H64V0zM480 0H448V32H480V0zM440 64H428C421.373 64 416 58.627 416 52V40H96V52C96 58.627 90.627 64 84 64H72V320H84C90.627 320 96 325.373 96 332V344H416V332C416 325.373 421.373 320 428 320H440V64zM404 256H320V308C320 314.628 314.627 320 308 320H108C101.373 320 96 314.628 96 308V140C96 133.372 101.373 128 108 128H192V76C192 69.372 197.373 64 204 64H404C410.627 64 416 69.372 416 76V244C416 250.628 410.627 256 404 256zM136 280H280V168H136V280zM376 104H232V128H308C314.627 128 320 133.372 320 140V216H376V104z" />
|
||||
<glyph glyph-name="object-ungroup"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M564 224C570.627 224 576 229.373 576 236V308C576 314.627 570.627 320 564 320H492C485.373 320 480 314.627 480 308V296H392V320H404C410.627 320 416 325.373 416 332V404C416 410.627 410.627 416 404 416H332C325.373 416 320 410.627 320 404V392H96V404C96 410.627 90.627 416 84 416H12C5.373 416 0 410.627 0 404V332C0 325.373 5.373 320 12 320H24V160H12C5.373 160 0 154.627 0 148V76C0 69.373 5.373 64 12 64H84C90.627 64 96 69.373 96 76V88H184V64H172C165.373 64 160 58.627 160 52V-20C160 -26.627 165.373 -32 172 -32H244C250.627 -32 256 -26.627 256 -20V-8H480V-20C480 -26.627 485.373 -32 492 -32H564C570.627 -32 576 -26.627 576 -20V52C576 58.627 570.627 64 564 64H552V224H564zM352 384H384V352H352V384zM352 128H384V96H352V128zM64 96H32V128H64V96zM64 352H32V384H64V352zM96 136V148C96 154.627 90.627 160 84 160H72V320H84C90.627 320 96 325.373 96 332V344H320V332C320 325.373 325.373 320 332 320H344V160H332C325.373 160 320 154.627 320 148V136H96zM224 0H192V32H224V0zM504 64H492C485.373 64 480 58.627 480 52V40H256V52C256 58.627 250.627 64 244 64H232V88H320V76C320 69.373 325.373 64 332 64H404C410.627 64 416 69.373 416 76V148C416 154.627 410.627 160 404 160H392V248H480V236C480 229.373 485.373 224 492 224H504V64zM544 0H512V32H544V0zM544 256H512V288H544V256z" />
|
||||
<glyph glyph-name="paper-plane"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M440 441.5L24 201.6C-10.4 181.7 -7.1 130.8 29.7 115.7L144 68.4V-16C144 -62.4 203.2 -81.5 230.6 -44.6L274.4 14.5L386.3 -31.7C392.2 -34.1 398.4 -35.3 404.6 -35.3C412.8 -35.3 420.9 -33.2 428.2 -29.1C441 -21.9 449.8 -9.1 452.1 5.4L511.4999999999999 392.6C517.5999999999999 432.7 474.6 461.4 440 441.5zM192 -16V48.6L228.6 33.5L192 -16zM404.6 12.7L250.8 76.2L391 278.5C401.7 294 381.5 312 367.3 299.7L155.8 115.4L48 160L464 400L404.6 12.7z" />
|
||||
<glyph glyph-name="pause-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119 440 8 329 8 192S119 -56 256 -56S504 55 504 192S393 440 256 440zM256 -8C145.5 -8 56 81.5 56 192S145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8zM352 272V112C352 103.2 344.8 96 336 96H288C279.2 96 272 103.2 272 112V272C272 280.8 279.2 288 288 288H336C344.8 288 352 280.8 352 272zM240 272V112C240 103.2 232.8 96 224 96H176C167.2 96 160 103.2 160 112V272C160 280.8 167.2 288 176 288H224C232.8 288 240 280.8 240 272z" />
|
||||
<glyph glyph-name="play-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M371.7 210L195.7 317C179.9 325.8 160 314.5 160 296V88C160 69.6 179.8 58.2 195.7 67L371.7 168C388.1 177.1 388.1 200.8 371.7 210zM504 192C504 329 393 440 256 440S8 329 8 192S119 -56 256 -56S504 55 504 192zM56 192C56 302.5 145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8S56 81.5 56 192z" />
|
||||
<glyph glyph-name="plus-square"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M352 208V176C352 169.4 346.6 164 340 164H252V76C252 69.4 246.6 64 240 64H208C201.4 64 196 69.4 196 76V164H108C101.4 164 96 169.4 96 176V208C96 214.6 101.4 220 108 220H196V308C196 314.6 201.4 320 208 320H240C246.6 320 252 314.6 252 308V220H340C346.6 220 352 214.6 352 208zM448 368V16C448 -10.5 426.5 -32 400 -32H48C21.5 -32 0 -10.5 0 16V368C0 394.5 21.5 416 48 416H400C426.5 416 448 394.5 448 368zM400 22V362C400 365.3 397.3 368 394 368H54C50.7 368 48 365.3 48 362V22C48 18.7 50.7 16 54 16H394C397.3 16 400 18.7 400 22z" />
|
||||
<glyph glyph-name="question-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119.043 440 8 328.9170000000001 8 192C8 55.003 119.043 -56 256 -56S504 55.003 504 192C504 328.9170000000001 392.957 440 256 440zM256 -8C145.468 -8 56 81.431 56 192C56 302.495 145.472 392 256 392C366.491 392 456 302.529 456 192C456 81.47 366.569 -8 256 -8zM363.2440000000001 247.2C363.2440000000001 180.148 290.8230000000001 179.116 290.8230000000001 154.337V148C290.8230000000001 141.373 285.4500000000001 136 278.8230000000001 136H233.1760000000001C226.5490000000001 136 221.1760000000001 141.373 221.1760000000001 148V156.659C221.1760000000001 192.404 248.2760000000001 206.693 268.7550000000001 218.175C286.3160000000001 228.02 297.0790000000001 234.716 297.0790000000001 247.754C297.0790000000001 265 275.0800000000001 276.447 257.2950000000001 276.447C234.1060000000001 276.447 223.4010000000001 265.4700000000001 208.3530000000001 246.478C204.2960000000001 241.358 196.8930000000001 240.407 191.6870000000001 244.354L163.8630000000001 265.452C158.7560000000001 269.324 157.6120000000001 276.5180000000001 161.2190000000001 281.815C184.846 316.509 214.94 336 261.794 336C310.865 336 363.244 297.6960000000001 363.244 247.2zM298 80C298 56.841 279.159 38 256 38S214 56.841 214 80S232.841 122 256 122S298 103.159 298 80z" />
|
||||
<glyph glyph-name="registered"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119.033 440 8 328.967 8 192S119.033 -56 256 -56S504 55.033 504 192S392.967 440 256 440zM256 -8C145.468 -8 56 81.451 56 192C56 302.531 145.451 392 256 392C366.532 392 456 302.549 456 192C456 81.468 366.549 -8 256 -8zM366.442 73.791C313.396 170.075 316.192 165.259 313.171 169.876C337.438 183.755 352.653 211.439 352.653 243.052C352.653 295.555 322.406 328.304 251.1550000000001 328.304H172.488C165.8710000000001 328.304 160.488 322.921 160.488 316.304V68C160.488 61.383 165.8710000000001 56 172.488 56H211.0560000000001C217.673 56 223.0560000000001 61.383 223.0560000000001 68V151.663H255.0140000000001L302.5290000000001 62.36A11.98 11.98 0 0 1 313.1220000000001 56H355.9320000000001C365.0720000000001 56 370.8460000000001 65.799 366.4420000000001 73.791zM256.933 208.094H223.058V272.234H250.435C282.852 272.234 289.3640000000001 260.101 289.3640000000001 240.525C289.3630000000001 219.612 277.846 208.094 256.9330000000001 208.094z" />
|
||||
<glyph glyph-name="sad-cry"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM392 53.6V168C392 181.2 381.2 192 368 192S344 181.2 344 168V16.6C315.5 1 282.8 -8 248 -8S180.5 1 152 16.6V168C152 181.2 141.2 192 128 192S104 181.2 104 168V53.6C69.4 89.6 48 138.3 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 138.3 426.6 89.5 392 53.6zM205.8 213.5C210.2 215.9 212.7 220.9 211.9 225.9C207.9 251.1 177.7 268 152.1 268S96.2 251.1 92.3 225.9C91.5 220.9 94 215.9 98.4 213.5C102.8 211.1 108.3 211.7 112.1 215.1L121.6 223.6C136.4 236.8 167.8 236.8 182.6 223.6L192.1 215.1C194.6 212.8 200 210.3 205.8 213.5zM344 268C318.3 268 288.1 251.1 284.2 225.9C283.4 220.9 285.9 215.9 290.3 213.5C294.8 211.1 300.2 211.7 304 215.1L313.5 223.6C328.3 236.8 359.7 236.8 374.5 223.6L384 215.1C386.5 212.9 392 210.4 397.7 213.5C402.1 215.9 404.6 220.9 403.8 225.9C399.9000000000001 251.1 369.7 268 344 268zM248 176C217.1 176 192 147.3 192 112S217.1 48 248 48S304 76.7 304 112S278.9 176 248 176z" />
|
||||
<glyph glyph-name="sad-tear"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM256 144C242.8 144 232 133.2 232 120S242.8 96 256 96C279.8 96 302.3 85.5 317.6 67.2C325.7000000000001 57.4 340.8 55.3 351.4000000000001 64.1C361.6 72.6 363.0000000000001 87.7 354.5000000000001 97.9C330 127.2 294.1 144 256 144zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272zM162.4 173.2C151 157.9 126 122.6 126 105.1C126 82.4 144.8 64 168 64S210 82.4 210 105.1C210 122.6 185 157.9 173.6 173.2000000000001C170.8 176.9 165.2 176.9 162.4 173.2000000000001z" />
|
||||
<glyph glyph-name="save"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M433.941 318.059L350.059 401.9410000000001A48 48 0 0 1 316.118 416H48C21.49 416 0 394.51 0 368V16C0 -10.51 21.49 -32 48 -32H400C426.51 -32 448 -10.51 448 16V284.118A48 48 0 0 1 433.941 318.059zM272 368V288H144V368H272zM394 16H54A6 6 0 0 0 48 22V362A6 6 0 0 0 54 368H96V264C96 250.745 106.745 240 120 240H296C309.255 240 320 250.745 320 264V364.118L398.243 285.875A6 6 0 0 0 400 281.632V22A6 6 0 0 0 394 16zM224 216C175.477 216 136 176.523 136 128S175.477 40 224 40S312 79.477 312 128S272.523 216 224 216zM224 88C201.944 88 184 105.944 184 128S201.944 168 224 168S264 150.056 264 128S246.056 88 224 88z" />
|
||||
<glyph glyph-name="share-square"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M561.938 289.94L417.94 433.908C387.926 463.922 336 442.903 336 399.968V342.77C293.55 340.89 251.97 336.2200000000001 215.24 324.7800000000001C180.07 313.8300000000001 152.17 297.2000000000001 132.33 275.36C108.22 248.8 96 215.4 96 176.06C96 114.363 129.178 63.605 180.87 31.3C218.416 7.792 266.118 43.951 251.89 87.04C236.375 134.159 234.734 157.963 336 165.8V112C336 69.007 387.968 48.087 417.94 78.06L561.938 222.06C580.688 240.8 580.688 271.2 561.938 289.94zM384 112V215.84C255.309 213.918 166.492 192.65 206.31 72C176.79 90.45 144 123.92 144 176.06C144 285.394 273.14 295.007 384 295.91V400L528 256L384 112zM408.74 27.507A82.658 82.658 0 0 1 429.714 36.81C437.69 41.762 448 35.984 448 26.596V-16C448 -42.51 426.51 -64 400 -64H48C21.49 -64 0 -42.51 0 -16V336C0 362.51 21.49 384 48 384H180C186.627 384 192 378.627 192 372V367.514C192 362.597 189.013 358.145 184.431 356.362C170.729 351.031 158.035 344.825 146.381 337.777A12.138 12.138 0 0 0 140.101 336H54A6 6 0 0 1 48 330V-10A6 6 0 0 1 54 -16H394A6 6 0 0 1 400 -10V15.966C400 21.336 403.579 26.025 408.74 27.507z" />
|
||||
<glyph glyph-name="smile-beam"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM332 135.4C311.2 110.4 280.5 96 248 96S184.8 110.3 164 135.4C155.5 145.6 140.4 146.9 130.2 138.5C120 130 118.7 114.9 127.1 104.7C157.1 68.7 201.2 48.1 248 48.1S338.9 68.7 368.9 104.7C377.4 114.9 376 130 365.8 138.5C355.6 146.9 340.5 145.6 332 135.4zM136.5 237C144.2 250.7 155.7 258.6 168 258.6S191.8 250.7 199.5 237L209 220C211.1 216.3 215.2 215.3 218.3 216.3C221.9 217.4 224.3 220.8 224 224.6C220.7 266.7000000000001 191.8 296 168 296S115.3 266.7000000000001 112 224.6C111.7 220.9 114.1 217.4 117.7 216.3C121.1 215.2 125.1 216.8 127 220L136.5 237zM328 296C304.2 296 275.3 266.7 272 224.6C271.7 220.9 274.1 217.4 277.7 216.3C281.2 215.2 285.1 216.8 287 220L296.5 237C304.2 250.7 315.7 258.6 328 258.6S351.8 250.7 359.5 237L369 220C371.1 216.3 375.2 215.3 378.3 216.3C381.9000000000001 217.4 384.3 220.8 384 224.6C380.7 266.7 351.8 296 328 296z" />
|
||||
<glyph glyph-name="smile-wink"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM365.8 138.4C355.6 146.9 340.5 145.5 332 135.3C311.2 110.3 280.5 95.9 248 95.9S184.8 110.2 164 135.3C155.5 145.5 140.3 146.8 130.2 138.4C120 129.9 118.7 114.8 127.1 104.6C157.1 68.6 201.2 47.9999999999999 248 47.9999999999999S338.9 68.6 368.9 104.6C377.4 114.8 376 129.9 365.8 138.4zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 268C302.3 268 272.1 251.1 268.1 225.9C266.4000000000001 214.7 279.6 207.7 287.9000000000001 215.1L297.4000000000001 223.6C312.2000000000001 236.8 343.6 236.8 358.4000000000001 223.6L367.9000000000001 215.1C376.4000000000001 207.7 389.5000000000001 214.8 387.7000000000001 225.9C383.9000000000001 251.1 353.7000000000001 268 328.0000000000001 268z" />
|
||||
<glyph glyph-name="smile"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM168 208C185.7 208 200 222.3 200 240S185.7 272 168 272S136 257.7 136 240S150.3 208 168 208zM328 208C345.7 208 360 222.3 360 240S345.7 272 328 272S296 257.7 296 240S310.3 208 328 208zM332 135.4C311.2 110.4 280.5 96 248 96S184.8 110.3 164 135.4C155.5 145.6 140.3 146.9 130.2 138.5C120 130 118.7 114.9 127.1 104.7C157.1 68.7 201.2 48.1 248 48.1S338.9 68.7 368.9 104.7C377.4 114.9 376 130 365.8 138.5C355.7 146.9 340.5 145.6 332 135.4z" />
|
||||
<glyph glyph-name="snowflake"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M440.1 92.8L400.9000000000001 115.8L435.0000000000001 125.1C443.4000000000001 127.4 448.4000000000001 136.2000000000001 446.1000000000001 144.7000000000001L442.0000000000001 160.2000000000001C439.8000000000001 168.7000000000001 431.1000000000001 173.8000000000001 422.7000000000001 171.5000000000001L343 149.8L271.2 192L343.1 234.2L422.8 212.5C431.2 210.2 439.8 215.3 442.1 223.8L446.2000000000001 239.3C448.4000000000001 247.8 443.5000000000001 256.6 435.1 258.9L401 268.2000000000001L440.2 291.2000000000001C447.7 295.6 450.3 305.4 446 313.1L438.1 327C433.8 334.7000000000001 424.1 337.3 416.6 332.9000000000001L377.4000000000001 309.9000000000001L386.5000000000001 344.6C388.7000000000001 353.1 383.8000000000001 361.9000000000001 375.4000000000001 364.2000000000001L360.2000000000001 368.3000000000001C351.8000000000001 370.6 343.2000000000001 365.5000000000001 340.9000000000001 357.0000000000001L319.6 276.0000000000001L247.7 233.8000000000001V318.3000000000001L306 377.6C312.1 383.8 312.1 394 306 400.2L294.9 411.5C288.8 417.7 278.8 417.7 272.7 411.5L247.8 386.1V432C247.8 440.8 240.8 448 232.1 448H216.4C207.7 448 200.7 440.8 200.7 432V385.9L175.8 411.3C169.7 417.5 159.7 417.5 153.6 411.3L142.1 400C136 393.8 136 383.6 142.1 377.4L200.4 318.1V233.6L128.5 275.8L107.2 356.8C105 365.3 96.3 370.4 87.9 368.1L72.7 364C64.3 361.7 59.3 352.9 61.6 344.4L70.7 309.7L31.5 332.7C24 337.1 14.4 334.5 10 326.8L2.1 312.9C-2.2 305.2 0.3 295.5 7.9 291L47.1 268L13 258.9C4.6 256.6 -0.4 247.8 1.9 239.3L6 223.8C8.2 215.3 16.9 210.2 25.3 212.5L105 234.2L176.9 192L105 149.8L25.3 171.5C16.9 173.8 8.3 168.7 6 160.2L1.9 144.7C-0.3 136.2 4.6 127.4 13 125.1L47.1 115.8L7.9 92.8C0.4 88.4 -2.2 78.6 2.1 70.9L10 57C14.3 49.3 24 46.7 31.5 51.1L70.7 74.1L61.6 39.4C59.4 30.9 64.3 22.1 72.7 19.8L87.9 15.7C96.3 13.4 104.9 18.5 107.2 27L128.5 108L200.4 150.2V65.7L142.1 6.4C136 0.2 136 -10 142.1 -16.2L153.2 -27.5000000000001C159.3 -33.7 169.3 -33.7 175.4 -27.5000000000001L200.3 -2.1000000000001V-48C200.3 -56.8 207.3 -64 216 -64H231.7C240.4 -64 247.4 -56.8 247.4 -48V-1.9L272.3 -27.3C278.4 -33.4999999999999 288.4 -33.4999999999999 294.5 -27.3L305.6 -15.9999999999999C311.7 -9.8 311.7 0.4 305.6 6.6000000000001L247.3 65.9000000000001V150.4000000000001L319.2 108.2000000000001L340.5 27.2000000000001C342.7 18.7000000000001 351.3999999999999 13.6000000000001 359.8 15.9000000000001L375 20C383.4 22.3 388.4 31.1 386.1 39.6L377 74.3L416.2 51.3C423.7 46.9 433.3 49.5 437.7 57.2L445.6 71.1C450.2 78.6 447.7 88.4 440.1 92.8z" />
|
||||
<glyph glyph-name="square"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M400 416H48C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48 -32H400C426.5 -32 448 -10.5 448 16V368C448 394.5 426.5 416 400 416zM394 16H54C50.7 16 48 18.7 48 22V362C48 365.3 50.7 368 54 368H394C397.3 368 400 365.3 400 362V22C400 18.7 397.3 16 394 16z" />
|
||||
<glyph glyph-name="star-half"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M288 62.7L163.7 -2.7L187.4 135.6999999999999L86.8 233.7L225.8 253.8999999999999L288 379.8999999999999V448C276.6 448 265.2 442.1 259.3 430.2L194 297.8L47.9 276.6C21.7 272.8 11.2 240.5 30.2 222L135.9 119L110.9 -26.5C106.4 -52.6 133.9 -72.5 157.3 -60.2L288 8.4V62.7z" />
|
||||
<glyph glyph-name="star"
|
||||
unicode=""
|
||||
horiz-adv-x="576" d=" M528.1 276.5L382 297.8L316.7 430.2C305 453.8 271.1 454.1 259.3 430.2L194 297.8L47.9 276.5C21.7 272.7 11.2 240.4 30.2 221.9L135.9 118.9L110.9 -26.6C106.4 -52.9 134.1 -72.6 157.3 -60.3L288 8.4L418.7 -60.3C441.9 -72.5 469.6 -52.9 465.1 -26.6L440.1 118.9L545.8 221.9C564.8 240.4 554.3 272.7 528.0999999999999 276.5zM388.6 135.7L412.3 -2.7L288 62.6L163.7 -2.7L187.4 135.7000000000001L86.8 233.7000000000001L225.8 253.9000000000001L288 379.9000000000001L350.2 253.9000000000001L489.2 233.7000000000001L388.6 135.7000000000001z" />
|
||||
<glyph glyph-name="sticky-note"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M448 99.894V368C448 394.51 426.51 416 400 416H48C21.49 416 0 394.51 0 368V16.012C0 -10.498 21.49 -31.988 48 -31.988H316.118A48 48 0 0 1 350.059 -17.929L433.941 65.953A48 48 0 0 1 448 99.894zM320 19.894V96.012H396.118L320 19.894zM400 368V144.012H296C282.745 144.012 272 133.267 272 120.012V16.012H48V368H400z" />
|
||||
<glyph glyph-name="stop-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M504 192C504 329 393 440 256 440S8 329 8 192S119 -56 256 -56S504 55 504 192zM56 192C56 302.5 145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8S56 81.5 56 192zM352 272V112C352 103.2 344.8 96 336 96H176C167.2 96 160 103.2 160 112V272C160 280.8 167.2 288 176 288H336C344.8 288 352 280.8 352 272z" />
|
||||
<glyph glyph-name="sun"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M494.2 226.1L434.4 266.6L448.1 337.6C450.7 350.8 446.5 364.4 437 374C427.3999999999999 383.5 413.8 387.7 400.8 385.1L329.9 371.4L289.5 431.3C274.4 453.6 237.6 453.6 222.5 431.3L182.1 371.4L111.3 385.1C98 387.6 84.5 383.5 75 373.9C65.5 364.3 61.3 350.8 63.9 337.6L77.6 266.6L17.8 226.1C6.6 218.5 0 206 0 192.5S6.7 166.5 17.8 159L77.6 118.5L63.9 47.5C61.3 34.3 65.5 20.7 75 11.2C84.5 1.7 97.9 -2.5 111.3 0.1L182.1 13.8L222.5 -46.1C230 -57.3 242.6 -64 256 -64S282 -57.3 289.5 -46.2L329.9 13.7L400.8 0C414.2 -2.7 427.6 1.6 437.1 11.1C446.6 20.6 450.7 34.2 448.2 47.4L434.5 118.4L494.3 158.9C505.4 166.4 512.1 179.0000000000001 512.1 192.4C512 206 505.4 218.5 494.2 226.1zM381.3 140.5L398.9 49.3L307.9 66.9L256 -10L204.1 67L113.2 49.4L130.8 140.6L54 192.6L130.8 244.6L113.2 335.8L204.2 318.2L256 395L307.9 318.1L398.9 335.7L381.3 244.6L458.1 192.6L381.3 140.5zM256 296C198.7 296 152 249.3 152 192S198.7 88 256 88S360 134.7 360 192S313.3 296 256 296zM256 136C225.1 136 200 161.1 200 192S225.1 248 256 248S312 222.9 312 192S286.9 136 256 136z" />
|
||||
<glyph glyph-name="surprise"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM248 168C212.7 168 184 139.3 184 104S212.7 40 248 40S312 68.7 312 104S283.3 168 248 168zM200 240C200 257.7 185.7 272 168 272S136 257.7 136 240S150.3 208 168 208S200 222.3 200 240zM328 272C310.3 272 296 257.7 296 240S310.3 208 328 208S360 222.3 360 240S345.7 272 328 272z" />
|
||||
<glyph glyph-name="thumbs-down"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M466.27 222.69C470.944 245.337 467.134 267.228 457.28 285.68C460.238 309.548 453.259 334.245 439.94 352.67C438.986 408.577 404.117 448 327 448C320 448 312 447.99 304.78 447.99C201.195 447.99 168.997 408 128 408H117.155C111.515 412.975 104.113 416 96 416H32C14.327 416 0 401.673 0 384V144C0 126.327 14.327 112 32 112H96C107.842 112 118.175 118.438 123.708 128H130.76C149.906 111.047 176.773 67.347 199.52 44.6C213.187 30.9330000000001 209.673 -64 271.28 -64C328.86 -64 366.55 -32.064 366.55 40.73C366.55 59.14 362.62 74.46 357.7 87.27H394.18C442.782 87.27 479.9999999999999 128.835 479.9999999999999 172.85C479.9999999999999 192 475.04 207.84 466.2699999999999 222.69zM64 152C50.745 152 40 162.745 40 176S50.745 200 64 200S88 189.255 88 176S77.255 152 64 152zM394.18 135.27H290.19C290.19 97.45 318.55 79.9 318.55 40.73C318.55 16.98 318.55 -16.0000000000001 271.2800000000001 -16.0000000000001C252.3700000000001 2.91 261.8200000000001 50.18 233.4600000000001 78.54C206.9 105.11 167.28 176 138.92 176H128V362.17C181.611 362.17 228.001 399.99 299.64 399.99H337.46C372.972 399.99 398.28 382.87 390.58 334.0900000000001C405.78 325.93 417.08 297.65 404.52 276.52C426.101 256.136 423.219 225.455 409.73 210.9C419.18 210.9 432.09 191.99 432 173.09C431.91 154.18 415.29 135.2700000000001 394.18 135.2700000000001z" />
|
||||
<glyph glyph-name="thumbs-up"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M466.27 161.31C475.04 176.16 480 192 480 211.15C480 255.165 442.782 296.73 394.18 296.73H357.7C362.62 309.54 366.55 324.86 366.55 343.27C366.55 416.064 328.86 448 271.28 448C209.673 448 213.187 353.067 199.52 339.4C176.773 316.653 149.905 272.953 130.76 256H32C14.327 256 0 241.673 0 224V-16C0 -33.673 14.327 -48 32 -48H96C110.893 -48 123.408 -37.826 126.978 -24.05C171.487 -25.051 202.038 -63.99 304.78 -63.99C312 -63.99 320 -64 327 -64C404.117 -64 438.986 -24.577 439.94 31.33C453.259 49.755 460.239 74.452 457.28 98.32C467.134 116.772 470.944 138.663 466.27 161.31zM404.52 107.48C417.08 86.35 405.78 58.0700000000001 390.58 49.91C398.28 1.13 372.972 -15.99 337.46 -15.99H299.64C228.001 -15.99 181.611 21.83 128 21.83V208H138.92C167.28 208 206.9 278.89 233.46 305.46C261.82 333.82 252.37 381.09 271.28 400C318.55 400 318.55 367.02 318.55 343.27C318.55 304.1 290.19 286.55 290.19 248.73H394.18C415.29 248.73 431.91 229.82 432 210.91C432.0899999999999 192.01 419.18 173.1 409.73 173.1C423.219 158.545 426.101 127.864 404.52 107.48zM88 16C88 2.745 77.255 -8 64 -8S40 2.745 40 16S50.745 40 64 40S88 29.255 88 16z" />
|
||||
<glyph glyph-name="times-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M256 440C119 440 8 329 8 192S119 -56 256 -56S504 55 504 192S393 440 256 440zM256 -8C145.5 -8 56 81.5 56 192S145.5 392 256 392S456 302.5 456 192S366.5 -8 256 -8zM357.8 254.2L295.6 192L357.8 129.8C362.5 125.1 362.5 117.5 357.8 112.8L335.2 90.2C330.5 85.5 322.9 85.5 318.2 90.2L256 152.4L193.8 90.2C189.1 85.5 181.5 85.5 176.8 90.2L154.2 112.8C149.5 117.5 149.5 125.1 154.2 129.8L216.4 192L154.2000000000001 254.2C149.5000000000001 258.9 149.5000000000001 266.5 154.2000000000001 271.2L176.8000000000001 293.8C181.5 298.5 189.1000000000001 298.5 193.8000000000001 293.8L256.0000000000001 231.6L318.2000000000001 293.8C322.9000000000001 298.5 330.5000000000001 298.5 335.2000000000001 293.8L357.8000000000001 271.2C362.5000000000001 266.5 362.5000000000001 258.9 357.8000000000001 254.2z" />
|
||||
<glyph glyph-name="tired"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C137.7 -8 48 81.7 48 192S137.7 392 248 392S448 302.3 448 192S358.3 -8 248 -8zM377.1 295.8C373.3 300.2000000000001 366.8 301.2000000000001 361.8 298.3L281.8 250.3C278.2 248.1 276 244.2 276 240S278.2 231.9 281.8 229.7L361.8 181.7C367.2 178.5 373.6 180.1 377.1 184.2C380.9000000000001 188.7 381 195.2 377.2000000000001 199.7L343.6 240L377.2000000000001 280.3C381.0000000000001 284.8 380.9000000000001 291.4 377.1 295.8zM220 240C220 244.2 217.8 248.1 214.2 250.3L134.2 298.3C129.2 301.3 122.7 300.2000000000001 118.9 295.8C115.1 291.3 115 284.8 118.8 280.3L152.4 240L118.8 199.7C115 195.2 115.1 188.7 118.9 184.2C122.4 180.1 128.8 178.5 134.2 181.7L214.2 229.7C217.8 231.9 220 235.8 220 240zM248 176C202.6 176 147.1 137.7 140.2 82.7C138.7 70.9 147.1 61.1 155.7 64.8C178.4 74.5 212 80 248 80S317.6 74.5 340.3 64.8C348.8 61.1 357.3 70.8 355.8 82.7C348.9000000000001 137.7 293.4000000000001 176 248 176z" />
|
||||
<glyph glyph-name="trash-alt"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M192 260V44C192 37.373 186.627 32 180 32H156C149.373 32 144 37.373 144 44V260C144 266.627 149.373 272 156 272H180C186.627 272 192 266.627 192 260zM292 272H268C261.373 272 256 266.627 256 260V44C256 37.373 261.373 32 268 32H292C298.627 32 304 37.373 304 44V260C304 266.627 298.627 272 292 272zM424 368C437.255 368 448 357.255 448 344V332C448 325.373 442.627 320 436 320H416V-16C416 -42.51 394.51 -64 368 -64H80C53.49 -64 32 -42.51 32 -16V320H12C5.373 320 0 325.373 0 332V344C0 357.255 10.745 368 24 368H98.411L132.429 424.6960000000001A48 48 0 0 0 173.589 448H274.412A48 48 0 0 0 315.572 424.6960000000001L349.589 368H424zM154.389 368H293.612L276.1600000000001 397.087A6 6 0 0 1 271.015 400H176.987A6 6 0 0 1 171.842 397.087L154.389 368zM368 320H80V-10A6 6 0 0 1 86 -16H362A6 6 0 0 1 368 -10V320z" />
|
||||
<glyph glyph-name="user-circle"
|
||||
unicode=""
|
||||
horiz-adv-x="496" d=" M248 344C195 344 152 301 152 248S195 152 248 152S344 195 344 248S301 344 248 344zM248 200C221.5 200 200 221.5 200 248S221.5 296 248 296S296 274.5 296 248S274.5 200 248 200zM248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM248 -8C198.3 -8 152.9 10.3 117.9 40.4C132.8 63.4 158.3 79 187.5 79.9C208.3 73.5 228.1 70.3 248 70.3S287.7 73.4 308.5 79.9C337.7 78.9 363.2 63.4 378.1 40.4C343.1 10.3 297.7000000000001 -8 248.0000000000001 -8zM410.7 76.1C386.3 107.5 348.6 128 305.6 128C295.4000000000001 128 279.6 118.4 248.0000000000001 118.4C216.5 118.4 200.6 128 190.4 128C147.5 128 109.8 107.5 85.3 76.1C61.9 108.8 48 148.8 48 192C48 302.3 137.7 392 248 392S448 302.3 448 192C448 148.8 434.1 108.8 410.7 76.1z" />
|
||||
<glyph glyph-name="user"
|
||||
unicode=""
|
||||
horiz-adv-x="448" d=" M313.6 144C284.9000000000001 144 271.1 128 224 128C176.9 128 163.2000000000001 144 134.4 144C60.2 144 0 83.8 0 9.6V-16C0 -42.5 21.5 -64 48 -64H400C426.5 -64 448 -42.5 448 -16V9.6C448 83.8 387.8 144 313.6 144zM400 -16H48V9.6C48 57.2000000000001 86.8 96 134.4 96C149 96 172.7 80 224 80C275.7 80 298.9 96 313.6 96C361.2000000000001 96 400 57.2 400 9.6V-16zM224 160C303.5 160 368 224.5 368 304S303.5 448 224 448S80 383.5 80 304S144.5 160 224 160zM224 400C276.9 400 320 356.9 320 304S276.9 208 224 208S128 251.1 128 304S171.1 400 224 400z" />
|
||||
<glyph glyph-name="window-close"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 416H48C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48 -32H464C490.5 -32 512 -10.5 512 16V368C512 394.5 490.5 416 464 416zM464 22C464 18.7 461.3 16 458 16H54C50.7 16 48 18.7 48 22V362C48 365.3 50.7 368 54 368H458C461.3 368 464 365.3 464 362V22zM356.5 253.4L295.1 192L356.5 130.6C361.1 126 361.1 118.5 356.5 113.8L334.2 91.5C329.6 86.9 322.1 86.9 317.4 91.5L256 152.9L194.6 91.5C190 86.9 182.5 86.9 177.8 91.5L155.5 113.8C150.9 118.4 150.9 125.9 155.5 130.6L216.9 192L155.5 253.4000000000001C150.9 258 150.9 265.5 155.5 270.2000000000001L177.8 292.5000000000001C182.4 297.1 189.9 297.1 194.6 292.5000000000001L256 231.1000000000001L317.4 292.5000000000001C322 297.1 329.5 297.1 334.2 292.5000000000001L356.5 270.2000000000001C361.2 265.6 361.2 258.1 356.5 253.4000000000001z" />
|
||||
<glyph glyph-name="window-maximize"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 416H48C21.5 416 0 394.5 0 368V16C0 -10.5 21.5 -32 48 -32H464C490.5 -32 512 -10.5 512 16V368C512 394.5 490.5 416 464 416zM464 22C464 18.7 461.3 16 458 16H54C50.7 16 48 18.7 48 22V256H464V22z" />
|
||||
<glyph glyph-name="window-minimize"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M480 -32H32C14.3 -32 0 -17.7 0 0S14.3 32 32 32H480C497.7 32 512 17.7 512 0S497.7 -32 480 -32z" />
|
||||
<glyph glyph-name="window-restore"
|
||||
unicode=""
|
||||
horiz-adv-x="512" d=" M464 448H144C117.5 448 96 426.5 96 400V352H48C21.5 352 0 330.5 0 304V-16C0 -42.5 21.5 -64 48 -64H368C394.5 -64 416 -42.5 416 -16V32H464C490.5 32 512 53.5 512 80V400C512 426.5 490.5 448 464 448zM368 -16H48V192H368V-16zM464 80H416V304C416 330.5 394.5 352 368 352H144V400H464V80z" />
|
||||
</font>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 136 KiB |
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 775 KiB |
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>py12306 购票助手</title><link href=../../static/css/app.7dba7f569524413218fde54c298188f4.css rel=stylesheet></head><body><script>window.config = {
|
||||
API_BASE_URL: 'http://localhost:8008/',
|
||||
}</script><div id=app></div><script type=text/javascript src=../../static/js/manifest.82f431004cf9bb6ad2cb.js></script><script type=text/javascript src=../../static/js/vendor.aebd1de04bf90e88d9c7.js></script><script type=text/javascript src=../../static/js/app.cdb00779aeb087dabd94.js></script></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
!function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a<e.length;a++)i=e[a],o[i]&&l.push(o[i][0]),o[i]=0;for(f in u)Object.prototype.hasOwnProperty.call(u,f)&&(r[f]=u[f]);for(n&&n(e,u,c);l.length;)l.shift()();if(c)for(a=0;a<c.length;a++)p=t(t.s=c[a]);return p};var e={},o={2:0};function t(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return r[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=r,t.c=e,t.d=function(r,n,e){t.o(r,n)||Object.defineProperty(r,n,{configurable:!1,enumerable:!0,get:e})},t.n=function(r){var n=r&&r.__esModule?function(){return r.default}:function(){return r};return t.d(n,"a",n),n},t.o=function(r,n){return Object.prototype.hasOwnProperty.call(r,n)},t.p="../../",t.oe=function(r){throw console.error(r),r}}([]);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,62 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from flask import Flask, request
|
||||
from flask_jwt_extended import (
|
||||
JWTManager)
|
||||
|
||||
from py12306.config import Config
|
||||
from py12306.helpers.func import *
|
||||
|
||||
|
||||
@singleton
|
||||
class Web:
|
||||
session = None
|
||||
jwt = None
|
||||
log = None
|
||||
|
||||
def __init__(self):
|
||||
self.session = Flask(__name__)
|
||||
self.log = logging.getLogger('werkzeug')
|
||||
self.log.setLevel(logging.ERROR)
|
||||
|
||||
self.register_blueprint()
|
||||
self.session.config['JWT_SECRET_KEY'] = 'secret' # 目前都是本地,暂不用放配置文件
|
||||
self.session.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(seconds=60 * 60 * 24 * 7) # Token 超时时间 7 天
|
||||
self.jwt = JWTManager(self.session)
|
||||
|
||||
def register_blueprint(self):
|
||||
from py12306.web.handler.user import user
|
||||
from py12306.web.handler.stat import stat
|
||||
from py12306.web.handler.app import app
|
||||
from py12306.web.handler.query import query
|
||||
from py12306.web.handler.log import log
|
||||
self.session.register_blueprint(user)
|
||||
self.session.register_blueprint(stat)
|
||||
self.session.register_blueprint(app)
|
||||
self.session.register_blueprint(query)
|
||||
self.session.register_blueprint(log)
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
self = cls()
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if not Config().WEB_ENABLE or Config().is_slave(): return
|
||||
# if Config().IS_DEBUG:
|
||||
# self.run_session()
|
||||
# else:
|
||||
create_thread_and_run(self, 'run_session', wait=False)
|
||||
|
||||
def run_session(self):
|
||||
debug = False
|
||||
if is_main_thread():
|
||||
debug = Config().IS_DEBUG
|
||||
self.session.run(debug=debug, port=Config().WEB_PORT, host='0.0.0.0')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Web.run()
|
||||
@@ -1,33 +1,23 @@
|
||||
-i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
|
||||
appdirs==1.4.3
|
||||
beautifulsoup4==4.7.0
|
||||
bs4==0.0.1
|
||||
certifi==2018.11.29
|
||||
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
aiohttp==3.6.2
|
||||
aiomysql==0.0.20
|
||||
aioredis==1.3.1
|
||||
aiosqlite==0.11.0
|
||||
async-timeout==3.0.1
|
||||
attrs==19.3.0
|
||||
cffi==1.13.2
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
cssselect==1.0.3
|
||||
fake-useragent==0.1.11
|
||||
Flask==1.0.2
|
||||
Flask-JWT-Extended==3.15.0
|
||||
ciso8601==2.1.2
|
||||
cryptography==2.8
|
||||
hiredis==1.0.1
|
||||
idna==2.8
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.10
|
||||
lxml==4.3.0
|
||||
MarkupSafe==1.1.0
|
||||
parse==1.9.0
|
||||
pyee==5.0.0
|
||||
PyJWT==1.7.1
|
||||
pyppeteer==0.0.25
|
||||
pyquery==1.4.0
|
||||
redis==3.0.1
|
||||
requests==2.21.0
|
||||
requests-html==0.9.0
|
||||
six==1.12.0
|
||||
soupsieve==1.6.2
|
||||
tqdm==4.28.1
|
||||
urllib3==1.24.2
|
||||
w3lib==1.19.0
|
||||
websockets==7.0
|
||||
Werkzeug==0.15.3
|
||||
DingtalkChatbot==1.3.0
|
||||
lightpush==0.1.3
|
||||
multidict==4.7.3
|
||||
pycparser==2.19
|
||||
pymysql==0.9.2
|
||||
pypika==0.35.18
|
||||
redis==3.3.11
|
||||
six==1.13.0
|
||||
toml==0.10.0
|
||||
tortoise-orm==0.15.4
|
||||
typing-extensions==3.7.4.1
|
||||
yarl==1.4.2
|
||||
|
||||
4
runtime/.gitignore
vendored
4
runtime/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!query
|
||||
!user
|
||||
2
runtime/query/.gitignore
vendored
2
runtime/query/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
runtime/user/.gitignore
vendored
2
runtime/user/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
41
tests/__init__.py
Normal file
41
tests/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from unittest import TestCase
|
||||
|
||||
from tortoise import Tortoise
|
||||
|
||||
from app.app import Config, App
|
||||
|
||||
|
||||
def async_test(func):
|
||||
""" Async test support """
|
||||
|
||||
@wraps(func)
|
||||
def warp(*args, **kwargs):
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(func(*args, **kwargs))
|
||||
|
||||
return warp
|
||||
|
||||
|
||||
async def __init_db():
|
||||
await Tortoise.init(db_url='sqlite://:memory:', modules={'models': ['app.models']})
|
||||
await Tortoise.generate_schemas()
|
||||
|
||||
|
||||
@async_test
|
||||
async def __init_test():
|
||||
await __init_db()
|
||||
Config.load()
|
||||
await App._App__load_data()
|
||||
|
||||
|
||||
__init_test()
|
||||
|
||||
|
||||
class BaseTest(TestCase):
|
||||
@classmethod
|
||||
@async_test
|
||||
async def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
await Tortoise.close_connections()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user