Telegram-Π±ΠΎΡ, HTTP API ΠΈ Π°Π²ΡΠΎΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΎΡ ΠΊΠΎΠ»ΠΎΠ΄ Hearthstone.
ΠΠΏΡΠ΅Π΄Π΅Π»ΡΠ΅Ρ ΠΊΠΎΠ΄Ρ ΠΊΠΎΠ»ΠΎΠ΄ Π² ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡΡ
, ΡΠ΅Π½Π΄Π΅ΡΠΈΡ ΠΊΡΠ°ΡΠΈΠ²ΡΠ΅ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΈ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΠΏΡΠ±Π»ΠΈΠΊΡΠ΅Ρ ΠΊΠΎΠ»ΠΎΠ΄Ρ ΡΡΡΠΈΠΌΠ΅ΡΠΎΠ² Π½Π° ΡΠ°ΠΉΡ ΠΈ Π² Telegram-ΠΊΠ°Π½Π°Π».
- πΌ ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄ β Π²ΡΡΠΎΠΊΠΎΠΊΠ°ΡΠ΅ΡΡΠ²Π΅Π½Π½ΡΠΉ ΡΠ΅Π½Π΄Π΅Ρ ΠΏΠΎ ΠΊΠΎΠ΄Ρ ΠΊΠΎΠ»ΠΎΠ΄Ρ Hearthstone
- π€ Telegram-Π±ΠΎΡ β Π°Π²ΡΠΎ-ΡΠ°ΡΠΏΠΎΠ·Π½Π°Π²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄ΠΎΠ² Π² ΡΠ°ΡΠ°Ρ ΠΈ Π»ΠΈΡΠ½ΡΡ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡΡ
- π HTTP API β ΠΏΡΠ±Π»ΠΈΡΠ½ΡΠ΅ endpoints Π΄Π»Ρ ΡΠ΅Π½Π΄Π΅ΡΠ° ΠΈ ΠΌΠ΅ΡΠ°Π΄Π°Π½Π½ΡΡ (Swagger UI ΠΈΠ· ΠΊΠΎΡΠΎΠ±ΠΊΠΈ)
- π WordPress-ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌΠΎΡΡΡ β CORS, ΠΏΡΡΠΌΠ°Ρ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΡ ΠΏΠΎΡΡΠΎΠ² ΡΠ΅ΡΠ΅Π· REST API, Π³ΠΎΡΠΎΠ²ΡΠ΅ ΡΠ½ΠΈΠΏΠΏΠ΅ΡΡ Π΄Π»Ρ ΡΠΎΡΡΠΊΠΎΠ΄ΠΎΠ²
- π ΠΡΠΎΡΡ-ΠΏΠ»Π°ΡΡΠΎΡΠΌΠ° β Π³ΠΎΡΠΎΠ²ΡΠ΅ ΠΊΠ»ΠΈΠ΅Π½ΡΡ Π΄Π»Ρ Python, JavaScript/TypeScript, PHP, Go, Ruby, C#, Rust, cURL ΠΈ shell
- π° ΠΠ²ΡΠΎΠΏΠΎΡΡΠΈΠ½Π³ Ρ HSGuru β ΠΊΠΎΠ»ΠΎΠ΄Ρ ΡΡΡΠΈΠΌΠ΅ΡΠΎΠ² ΠΏΡΠ±Π»ΠΈΠΊΡΡΡΡΡ Π½Π° WordPress-ΡΠ°ΠΉΡ ΠΈ Π² ΠΊΠ°Π½Π°Π»
- π ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ Π°ΡΡ Π΅ΡΠΈΠΏΠΎΠ² β Π²ΡΡΡΠΎΠ΅Π½Π½Π°Ρ ΡΠ°Π±Π»ΠΈΡΠ° EN β RU Π΄Π»Ρ Π½Π°Π·Π²Π°Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄
- π‘ ΠΠ°ΡΠΈΡΠ° ΠΎΡ Π΄ΡΠ±Π»ΠΈΠΊΠ°ΡΠΎΠ² β ΠΏΡΠΎΠ²Π΅ΡΠΊΠ° ΠΏΠΎ ΠΊΠΎΠ΄Ρ, ΡΡ ΠΎΠΆΠ΅ΡΡΠΈ ΠΊΠ°ΡΡ (Jaccard β₯ 90%) ΠΈ Π½Π°Π·Π²Π°Π½ΠΈΡ
- β‘ Blizzard API + ΠΊΡΡ β Π°ΠΊΡΡΠ°Π»ΡΠ½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅ ΠΊΠ°ΡΡ Ρ Π»ΠΎΠΊΠ°Π»ΡΠ½ΡΠΌ ΠΊΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ
- π ΠΠ΄ΠΌΠΈΠ½-ΠΏΠ°Π½Π΅Π»Ρ β ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠ΅ΡΠ΅Π· Π²Π΅Π±-ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ ΠΈ ΠΊΠΎΠΌΠ°Π½Π΄Ρ Π±ΠΎΡΠ°
- ΠΡΠ±Π»ΠΈΡΠ½ΠΎΠ΅ API
- Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΠΈ Π½Π°ΡΡΡΠΎΠΉΠΊΠ°
- ΠΠ°ΠΏΡΡΠΊ
- Π‘ΡΡΡΠΊΡΡΡΠ° ΠΏΡΠΎΠ΅ΠΊΡΠ°
- ΠΠΎΠΌΠ°Π½Π΄Ρ Π±ΠΎΡΠ°
- ΠΡΠ°Π²ΠΈΠ»Π° ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ ΠΊΠΎΠ»ΠΎΠ΄
- ΠΠ»ΠΈΠ΅Π½ΡΡ Π½Π° ΡΠ°Π·Π½ΡΡ ΡΠ·ΡΠΊΠ°Ρ
- ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ Ρ WordPress
- ΠΠΈΠ°Π³Π½ΠΎΡΡΠΈΠΊΠ°
- ΠΠΈΡΠ΅Π½Π·ΠΈΡ
ΠΡΠ±Π»ΠΈΡΠ½ΠΎΠ΅ API Π΄ΠΎΡΡΡΠΏΠ½ΠΎ Π±Π΅Π· Π°Π²ΡΠΎΡΠΈΠ·Π°ΡΠΈΠΈ ΠΏΠΎ Π°Π΄ΡΠ΅ΡΡ ΡΠ΅ΡΠ²Π΅ΡΠ°. ΠΡΠ΅ ΠΏΡΠ±Π»ΠΈΡΠ½ΡΠ΅ endpoints ΠΈΠΌΠ΅ΡΡ ΠΏΡΠ΅ΡΠΈΠΊΡ /public/.
ΠΠ½ΡΠ΅ΡΠ°ΠΊΡΠΈΠ²Π½Π°Ρ Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΡ Swagger UI Π΄ΠΎΡΡΡΠΏΠ½Π° ΠΏΠΎ Π°Π΄ΡΠ΅ΡΡ:
https://your-server/docs
ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅Ρ PNG-ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ ΠΏΠΎ Π΅Ρ ΠΊΠΎΠ΄Ρ.
GET /public/render?deck=<ΠΊΠΎΠ΄_ΠΊΠΎΠ»ΠΎΠ΄Ρ>
ΠΠ°ΡΠ°ΠΌΠ΅ΡΡΡ:
| ΠΠ°ΡΠ°ΠΌΠ΅ΡΡ | Π’ΠΈΠΏ | ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΡΠΉ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|---|---|
deck |
string | β | ΠΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ Hearthstone (Π½Π°ΡΠΈΠ½Π°Π΅ΡΡΡ Ρ AAE) |
ΠΡΠΈΠΌΠ΅Ρ Π·Π°ΠΏΡΠΎΡΠ°:
curl "https://your-server/public/render?deck=AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA" \
--output deck.pngΠΡΠΈΠΌΠ΅Ρ Π½Π° Python:
import requests
deck_code = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
response = requests.get(
"https://your-server/public/render",
params={"deck": deck_code}
)
with open("deck.png", "wb") as f:
f.write(response.content)ΠΡΠΈΠΌΠ΅Ρ Π½Π° JavaScript:
const deckCode = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
const response = await fetch(`https://your-server/public/render?deck=${deckCode}`);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// url ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡΠΈΡΠ²ΠΎΠΈΡΡ src Ρ <img>ΠΡΠ²Π΅Ρ: PNG-ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ (image/png)
ΠΠΎΠ΄Ρ ΠΎΡΠΈΠ±ΠΎΠΊ:
| ΠΠΎΠ΄ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|
| 200 | Π£ΡΠΏΠ΅Ρ β Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ PNG |
| 400 | ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΈΠ»ΠΈ Π½Π΅ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅ΠΌΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ |
ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ JSON Ρ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΠ΅ΠΉ ΠΎ ΠΊΠΎΠ»ΠΎΠ΄Π΅: ΠΊΠ»Π°ΡΡ, ΡΠΎΡΠΌΠ°Ρ, ΡΡΠΎΠΈΠΌΠΎΡΡΡ ΠΏΡΠ»ΠΈ, ΡΠΏΠΈΡΠΎΠΊ ΠΊΠ°ΡΡ.
GET /public/meta?deck=<ΠΊΠΎΠ΄_ΠΊΠΎΠ»ΠΎΠ΄Ρ>
ΠΠ°ΡΠ°ΠΌΠ΅ΡΡΡ:
| ΠΠ°ΡΠ°ΠΌΠ΅ΡΡ | Π’ΠΈΠΏ | ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΡΠΉ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|---|---|
deck |
string | β | ΠΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ Hearthstone (Π½Π°ΡΠΈΠ½Π°Π΅ΡΡΡ Ρ AAE) |
ΠΡΠΈΠΌΠ΅Ρ Π·Π°ΠΏΡΠΎΡΠ°:
curl "https://your-server/public/meta?deck=AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"ΠΡΠΈΠΌΠ΅Ρ ΠΎΡΠ²Π΅ΡΠ°:
{
"deck_class": "ΠΡΠ΅Ρ",
"deck_format": "Π‘ΡΠ°Π½Π΄Π°ΡΡ",
"dust_cost": 11200,
"card_count": 30,
"cards": [
{ "dbf_id": 90749, "name": "E.T.C., Band Manager", "name_ru": "Π.Π’.Π‘., ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ Π³ΡΡΠΏΠΏΡ", "cost": 3, "count": 1, "rarity": "LEGENDARY" },
...
]
}ΠΡΠΈΠΌΠ΅Ρ Π½Π° Python:
import requests
deck_code = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
meta = requests.get(
"https://your-server/public/meta",
params={"deck": deck_code}
).json()
print(f"ΠΠ»Π°ΡΡ: {meta['deck_class']}")
print(f"Π€ΠΎΡΠΌΠ°Ρ: {meta['deck_format']}")
print(f"Π‘ΡΠΎΠΈΠΌΠΎΡΡΡ ΠΏΡΠ»ΠΈ: {meta['dust_cost']}")ΠΠΎΠ΄Ρ ΠΎΡΠΈΠ±ΠΎΠΊ:
| ΠΠΎΠ΄ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|
| 200 | Π£ΡΠΏΠ΅Ρ β Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ JSON |
| 400 | ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΈΠ»ΠΈ Π½Π΅ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅ΠΌΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ |
ΠΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ ΠΏΠΎΠ»Π½ΡΡ ΡΠ°Π±Π»ΠΈΡΡ ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄ΠΎΠ² Π½Π°Π·Π²Π°Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄ Ρ Π°Π½Π³Π»ΠΈΠΉΡΠΊΠΎΠ³ΠΎ Π½Π° ΡΡΡΡΠΊΠΈΠΉ.
GET /public/archetypes
ΠΠ°ΡΠ°ΠΌΠ΅ΡΡΡ: Π½Π΅Ρ
ΠΡΠΈΠΌΠ΅Ρ Π·Π°ΠΏΡΠΎΡΠ°:
curl "https://your-server/public/archetypes"ΠΡΠΈΠΌΠ΅Ρ ΠΎΡΠ²Π΅ΡΠ°:
[
{ "eng": "Control Warrior", "rus": "ΠΠΎΠ½ΡΡΠΎΠ»Ρ ΠΠΎΠΈΠ½" },
{ "eng": "Miracle Rogue", "rus": "ΠΠΈΡΠ°ΠΊΠ»Π» Π Π°Π·Π±ΠΎΠΉΠ½ΠΈΠΊ" },
{ "eng": "Big Spell Mage", "rus": "ΠΠΎΠ»ΡΡΠΈΠ΅ Π·Π°ΠΊΠ»ΠΈΠ½Π°Π½ΠΈΡ ΠΠ°Π³" },
...
]ΠΡΠΈΠΌΠ΅Ρ Π½Π° Python:
import requests
archetypes = requests.get("https://your-server/public/archetypes").json()
# Π‘ΡΡΠΎΠΈΠΌ ΡΠ»ΠΎΠ²Π°ΡΡ eng -> rus
translation_map = {item["eng"]: item["rus"] for item in archetypes}
print(translation_map.get("Control Warrior", "ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"))
# β "ΠΠΎΠ½ΡΡΠΎΠ»Ρ ΠΠΎΠΈΠ½"ΠΡΠΈΠΌΠ΅Ρ Π½Π° JavaScript:
const archetypes = await fetch("https://your-server/public/archetypes").then(r => r.json());
const map = Object.fromEntries(archetypes.map(a => [a.eng, a.rus]));
console.log(map["Control Warrior"]); // "ΠΠΎΠ½ΡΡΠΎΠ»Ρ ΠΠΎΠΈΠ½"Π‘Ρ Π΅ΠΌΠ° ΡΠ»Π΅ΠΌΠ΅Π½ΡΠ°:
| ΠΠΎΠ»Π΅ | Π’ΠΈΠΏ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|---|
eng |
string | ΠΠ½Π³Π»ΠΈΠΉΡΠΊΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π°ΡΡ Π΅ΡΠΈΠΏΠ° |
rus |
string | Π ΡΡΡΠΊΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π°ΡΡ Π΅ΡΠΈΠΏΠ° |
ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ΠΈΡ ΠΎΠ΄Π½ΠΎ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ Ρ Π°Π½Π³Π»ΠΈΠΉΡΠΊΠΎΠ³ΠΎ Π½Π° ΡΡΡΡΠΊΠΈΠΉ. ΠΡΠ»ΠΈ ΡΠΎΡΠ½ΠΎΠ³ΠΎ ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄Π° Π½Π΅Ρ β Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ ΠΎΡΠΈΠ³ΠΈΠ½Π°Π».
POST /public/archetypes/translate
Content-Type: application/json
Π’Π΅Π»ΠΎ Π·Π°ΠΏΡΠΎΡΠ°:
{ "name": "Control Warrior" }| ΠΠΎΠ»Π΅ | Π’ΠΈΠΏ | ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΡΠΉ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|---|---|
name |
string | β | ΠΠ°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ Π΄Π»Ρ ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄Π° |
ΠΡΠΈΠΌΠ΅Ρ Π·Π°ΠΏΡΠΎΡΠ°:
curl -X POST "https://your-server/public/archetypes/translate" \
-H "Content-Type: application/json" \
-d '{"name": "Control Warrior"}'ΠΡΠΈΠΌΠ΅Ρ ΠΎΡΠ²Π΅ΡΠ° (ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄ Π½Π°ΠΉΠ΄Π΅Π½):
{
"original": "Control Warrior",
"translated": "ΠΠΎΠ½ΡΡΠΎΠ»Ρ ΠΠΎΠΈΠ½",
"changed": true
}ΠΡΠΈΠΌΠ΅Ρ ΠΎΡΠ²Π΅ΡΠ° (ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½):
{
"original": "Some Unknown Deck",
"translated": "Some Unknown Deck",
"changed": false
}ΠΡΠΈΠΌΠ΅Ρ Π½Π° Python:
import requests
response = requests.post(
"https://your-server/public/archetypes/translate",
json={"name": "Big Spell Mage"}
).json()
if response["changed"]:
print(f"ΠΠ΅ΡΠ΅Π²Π΅Π΄Π΅Π½ΠΎ: {response['translated']}")
else:
print("ΠΠ΅ΡΠ΅Π²ΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½, ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌ ΠΎΡΠΈΠ³ΠΈΠ½Π°Π»")ΠΡΠΈΠΌΠ΅Ρ Π½Π° JavaScript:
const result = await fetch("https://your-server/public/archetypes/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Big Spell Mage" })
}).then(r => r.json());
console.log(result.translated); // "ΠΠΎΠ»ΡΡΠΈΠ΅ Π·Π°ΠΊΠ»ΠΈΠ½Π°Π½ΠΈΡ ΠΠ°Π³"
console.log(result.changed); // trueΠ‘Ρ Π΅ΠΌΠ° ΠΎΡΠ²Π΅ΡΠ°:
| ΠΠΎΠ»Π΅ | Π’ΠΈΠΏ | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|---|
original |
string | ΠΡΡ ΠΎΠ΄Π½ΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ |
translated |
string | ΠΠ΅ΡΠ΅Π²Π΅Π΄ΡΠ½Π½ΠΎΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ (ΠΈΠ»ΠΈ ΠΎΡΠΈΠ³ΠΈΠ½Π°Π») |
changed |
boolean | true Π΅ΡΠ»ΠΈ ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄ Π±ΡΠ» Π½Π°ΠΉΠ΄Π΅Π½ |
- Python 3.10β3.13 (ΠΏΡΠΎΡΠ΅ΡΡΠΈΡΠΎΠ²Π°Π½ΠΎ Π½Π° 3.13)
- ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΊΠ°ΡΡ Hearthstone (ΠΏΠ°ΠΏΠΊΠ°
cards/, ΡΠ°ΠΉΠ»Ρ Π²ΠΈΠ΄Π°SW_001.png) cards.jsonΠΎΡ HearthstoneJSON β Π»ΠΈΠ±ΠΎ Π²ΠΊΠ»ΡΡΡΠ½Π½ΡΠΉ Blizzard API- Π‘Π²ΠΎΠ±ΠΎΠ΄Π½ΡΠΉ ΠΏΠΎΡΡ
8000(Π΄Π»Ρ HTTP API) ΠΈ/ΠΈΠ»ΠΈ ΠΈΠ½ΡΠ΅ΡΠ½Π΅Ρ (Π΄Π»Ρ Telegram-Π±ΠΎΡΠ°)
git clone https://github.com/Zulut30/deckview-telegram-bot.git
cd deckview-telegram-botΠ Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡΠ΅ΡΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ Π²ΠΈΡΡΡΠ°Π»ΡΠ½ΠΎΠ΅ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΠ΅:
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txtπ‘ ΠΠ° Python 3.13 ΡΠ±Π΅Π΄ΠΈΡΠ΅ΡΡ, ΡΡΠΎ Ρ Π²Π°Ρ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½
setuptools(pip install setuptools) β Π½Π΅ΠΊΠΎΡΠΎΡΡΠ΅ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠΈ Π΅Π³ΠΎ ΡΡΠ΅Π±ΡΡΡ.
Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΡΠ°ΠΉΠ» .env (ΡΠΊΠΎΠΏΠΈΡΡΠΉΡΠ΅ ΠΈΠ· .env.example):
cp .env.example .envΠΠ°ΠΏΠΎΠ»Π½ΠΈΡΠ΅ ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΠ΅:
# ΠΠ±ΡΠ·Π°ΡΠ΅Π»ΡΠ½ΠΎ
BOT_TOKEN=Π²Π°Ρ_ΡΠΎΠΊΠ΅Π½_Π±ΠΎΡΠ° # Π’ΠΎΠΊΠ΅Π½ ΠΎΡ @BotFather
# ΠΡΡΠΈ ΠΊ Π΄Π°Π½Π½ΡΠΌ ΠΊΠ°ΡΡ
IMAGES_PATH=cards # ΠΠ°ΠΏΠΊΠ° Ρ PNG ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡΠΌΠΈ ΠΊΠ°ΡΡ
JSON_PATH=cards.json # ΠΠ°Π·Π° ΠΊΠ°ΡΡ (HearthstoneJSON)
JSON_RU_PATH=cardsRU.json # ΠΠ°Π·Π° Ρ ΡΡΡΡΠΊΠΈΠΌΠΈ Π½Π°Π·Π²Π°Π½ΠΈΡΠΌΠΈ
# WordPress (Π΄Π»Ρ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ Π½Π° ΡΠ°ΠΉΡ)
WP_BASE_URL=https://your-site.com
WP_USER=wordpress_user
WP_APP_PASSWORD=app_password
WP_UPLOAD_ENABLED=1
# Telegram-ΠΊΠ°Π½Π°Π» Π΄Π»Ρ Π°Π²ΡΠΎΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)
CHANNEL_ID=@your_channel # ΠΈΠ»ΠΈ ΡΠΈΡΠ»ΠΎΠ²ΠΎΠΉ ID: -1001234567890
ADMIN_IDS=123456789 # Telegram ID Π°Π΄ΠΌΠΈΠ½ΠΈΡΡΡΠ°ΡΠΎΡΠΎΠ² (ΡΠ΅ΡΠ΅Π· Π·Π°ΠΏΡΡΡΡ)
# API-ΠΊΠ»ΡΡ Π΄Π»Ρ ΠΏΡΠΈΠ²Π°ΡΠ½ΡΡ
endpoints (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ)
# ΠΡΠ»ΠΈ Π½Π΅ Π·Π°Π΄Π°Π½ β ΠΏΡΠΈΠ²Π°ΡΠ½ΡΠ΅ /render, /meta, /ingest ΡΠΎΠΆΠ΅ Π΄ΠΎΡΡΡΠΏΠ½Ρ Π±Π΅Π· ΠΊΠ»ΡΡΠ°
API_KEY=
# HSGuru ΠΏΠ°ΡΡΠ΅Ρ (Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠΉ ΠΏΠΎΡΡΠΈΠ½Π³ ΠΊΠΎΠ»ΠΎΠ΄ ΡΡΡΠΈΠΌΠ΅ΡΠΎΠ²)
HSGURU_ENABLED=1
HSGURU_URL=https://www.hsguru.com/streamer-decks
HSGURU_INTERVAL_SECONDS=1800 # ΠΠ½ΡΠ΅ΡΠ²Π°Π» ΠΌΠ΅ΠΆΠ΄Ρ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΡΠΌΠΈ (30 ΠΌΠΈΠ½)
HSGURU_SEEN_PATH=cache/hsguru_seen.json
# Blizzard API (ΠΎΠΏΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ, Π΄Π»Ρ Π°ΠΊΡΡΠ°Π»ΡΠ½ΡΡ
Π΄Π°Π½Π½ΡΡ
ΠΊΠ°ΡΡ)
BLIZZARD_ENABLED=1
BLIZZARD_CLIENT_ID=your_client_id
BLIZZARD_CLIENT_SECRET=your_client_secret
BLIZZARD_REGION=eu
BLIZZARD_LOCALE=en_US
BLIZZARD_LOCALE_RU=ru_RU
BLIZZARD_CACHE_DIR=cache/blizzard
BLIZZARD_CACHE_TTL_HOURS=24python bot.pyuvicorn api:app --host 0.0.0.0 --port 8000ΠΠΎΡΠ»Π΅ Π·Π°ΠΏΡΡΠΊΠ° ΠΏΡΠΎΠ²Π΅ΡΡΡΠ΅, ΡΡΠΎ Π²ΡΡ ΠΏΠΎΠ΄Π½ΡΠ»ΠΎΡΡ:
curl http://localhost:8000/public/archetypes | head -c 200
# Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡΠΈΠΉΡΠΈ JSON-ΠΌΠ°ΡΡΠΈΠ² Ρ ΠΏΠ°ΡΠ°ΠΌΠΈ {"eng": ..., "rus": ...}ΠΠ½ΡΠ΅ΡΠ°ΠΊΡΠΈΠ²Π½Π°Ρ Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΡ Swagger UI Π±ΡΠ΄Π΅Ρ Π΄ΠΎΡΡΡΠΏΠ½Π° Π½Π° http://localhost:8000/docs,
ReDoc β Π½Π° http://localhost:8000/redoc.
# 1. ΠΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΡ ΠΎΡΠΊΡΡΠ²Π°Π΅ΡΡΡ
curl -sf http://localhost:8000/docs > /dev/null && echo "β docs OK"
# 2. JSON-ΡΠ½Π΄ΠΏΠΎΠΈΠ½Ρ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ
curl -sf http://localhost:8000/public/archetypes > /dev/null && echo "β archetypes OK"
# 3. ΠΠ΅Π²Π°Π»ΠΈΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ β 400
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:8000/public/meta?deck=bad"
# ΠΎΠΆΠΈΠ΄Π°Π΅ΠΌ: 400
# 4. Π Π΅Π°Π»ΡΠ½ΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ β 200 (ΡΡΠ΅Π±ΡΠ΅ΡΡΡ Π·Π°Π³ΡΡΠΆΠ΅Π½Π½Π°Ρ Π±Π°Π·Π° ΠΊΠ°ΡΡ)
curl -s -o /dev/null -w "%{http_code}\n" \
"http://localhost:8000/public/render?deck=AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
# ΠΎΠΆΠΈΠ΄Π°Π΅ΠΌ: 200ΠΠ΅ ΠΏΡΠ±Π»ΠΈΠΊΡΠΉΡΠ΅ uvicorn Π½Π°ΠΏΡΡΠΌΡΡ Π² ΠΈΠ½ΡΠ΅ΡΠ½Π΅Ρ. ΠΠΎΡΡΠ°Π²ΡΡΠ΅ ΠΏΠ΅ΡΠ΅Π΄ Π½ΠΈΠΌ nginx ΠΈ terminate TLS:
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ /etc/systemd/system/deckview-bot.service (Telegram-Π±ΠΎΡ):
[Unit]
Description=Deckview Telegram Bot
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/tg-manacost-bot
ExecStart=/home/ubuntu/tg-manacost-bot/venv/bin/python bot.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetΠ /etc/systemd/system/deckview-api.service (HTTP API):
[Unit]
Description=Deckview HTTP API
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/tg-manacost-bot
ExecStart=/home/ubuntu/tg-manacost-bot/venv/bin/uvicorn api:app \
--host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now deckview-bot deckview-api
sudo systemctl status deckview-bot deckview-apipython update_cards.pyAPI β ΡΡΠΎ ΠΎΠ±ΡΡΠ½ΡΠΉ HTTP/JSON, ΠΏΠΎΡΡΠΎΠΌΡ ΡΠ°Π±ΠΎΡΠ°Π΅Ρ ΠΈΠ· Π»ΡΠ±ΠΎΠ³ΠΎ ΡΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΠΎΠ³ΠΎ ΡΠ·ΡΠΊΠ° ΠΈ ΡΡΠ΅ΠΉΠΌΠ²ΠΎΡΠΊΠ°. ΠΠΈΠΆΠ΅ β Π³ΠΎΡΠΎΠ²ΡΠ΅ ΡΠ½ΠΈΠΏΠΏΠ΅ΡΡ Π΄Π»Ρ ΡΠ°ΠΌΡΡ
ΠΏΠΎΠΏΡΠ»ΡΡΠ½ΡΡ
. ΠΠ΅Π·Π΄Π΅ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ ΡΠΎΡ ΠΆΠ΅ ΡΠ΅ΡΡΠΎΠ²ΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ; Π·Π°ΠΌΠ΅Π½ΠΈΡΠ΅ https://api.example.com Π½Π° Π°Π΄ΡΠ΅Ρ Π²Π°ΡΠ΅Π³ΠΎ ΡΠ΅ΡΠ²Π΅ΡΠ°.
π Python (requests / httpx)
import requests
DECK = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
BASE = "https://api.example.com"
# ΠΠΎΠ»ΡΡΠΈΡΡ ΠΌΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
meta = requests.get(f"{BASE}/public/meta", params={"deck": DECK}, timeout=15).json()
print(meta["deck_class"], meta["dust_cost"])
# Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ PNG
img = requests.get(f"{BASE}/public/render", params={"deck": DECK}, timeout=30)
img.raise_for_status()
open("deck.png", "wb").write(img.content)ΠΡΠΈΠ½Ρ
ΡΠΎΠ½Π½ΠΎ ΡΠ΅ΡΠ΅Π· httpx:
import httpx, asyncio
async def fetch():
async with httpx.AsyncClient(base_url="https://api.example.com", timeout=15) as c:
r = await c.get("/public/meta", params={"deck": DECK})
return r.json()
print(asyncio.run(fetch()))π¨ JavaScript (Π±ΡΠ°ΡΠ·Π΅Ρ / Node.js fetch)
const BASE = "https://api.example.com";
const DECK = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
// ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
const meta = await fetch(`${BASE}/public/meta?deck=${encodeURIComponent(DECK)}`).then(r => r.json());
console.log(meta.deck_class, meta.dust_cost);
// ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅
const blob = await fetch(`${BASE}/public/render?deck=${encodeURIComponent(DECK)}`).then(r => r.blob());
document.querySelector("#deck").src = URL.createObjectURL(blob);Π Node.js 18+ fetch ΡΠ°Π±ΠΎΡΠ°Π΅Ρ ΠΈΠ· ΠΊΠΎΡΠΎΠ±ΠΊΠΈ. ΠΠ»Ρ CommonJS:
const res = await fetch(`${BASE}/public/meta?deck=${encodeURIComponent(DECK)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(await res.json());π¦ TypeScript (Ρ ΡΠΈΠΏΠ°ΠΌΠΈ)
interface DeckCard {
dbf_id: number;
name: string;
name_ru: string;
cost: number;
count: number;
rarity: string;
}
interface DeckMeta {
deck_class: string;
deck_format: string;
dust_cost: number;
card_count: number;
cards: DeckCard[];
}
async function getDeckMeta(deckCode: string): Promise<DeckMeta> {
const res = await fetch(`https://api.example.com/public/meta?deck=${encodeURIComponent(deckCode)}`);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json() as Promise<DeckMeta>;
}π PHP (Π½Π°ΡΠΈΠ²Π½ΡΠΉ + Guzzle + Laravel)
<?php
$deck = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
$base = "https://api.example.com";
// ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅ ΡΠ΅ΡΠ΅Π· file_get_contents
$meta = json_decode(file_get_contents("$base/public/meta?deck=" . urlencode($deck)), true);
echo $meta['deck_class'], ' / ΠΡΠ»Ρ: ', $meta['dust_cost'];
// PNG ΡΠ΅ΡΠ΅Π· cURL
$ch = curl_init("$base/public/render?deck=" . urlencode($deck));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 30]);
file_put_contents("deck.png", curl_exec($ch));
curl_close($ch);Π§Π΅ΡΠ΅Π· Guzzle:
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://api.example.com', 'timeout' => 15]);
$meta = json_decode($client->get('/public/meta', ['query' => ['deck' => $deck]])->getBody(), true);Π§Π΅ΡΠ΅Π· Laravel HTTP:
use Illuminate\Support\Facades\Http;
$meta = Http::timeout(15)->get('https://api.example.com/public/meta', ['deck' => $deck])->json();πΉ Go
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
type DeckMeta struct {
DeckClass string `json:"deck_class"`
DeckFormat string `json:"deck_format"`
DustCost int `json:"dust_cost"`
CardCount int `json:"card_count"`
}
func main() {
deck := "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
base := "https://api.example.com"
// ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
resp, err := http.Get(base + "/public/meta?deck=" + url.QueryEscape(deck))
if err != nil { panic(err) }
defer resp.Body.Close()
var meta DeckMeta
json.NewDecoder(resp.Body).Decode(&meta)
fmt.Printf("%s / %d ΠΏΡΠ»ΠΈ\n", meta.DeckClass, meta.DustCost)
// PNG
img, _ := http.Get(base + "/public/render?deck=" + url.QueryEscape(deck))
defer img.Body.Close()
out, _ := os.Create("deck.png")
io.Copy(out, img.Body)
out.Close()
}π Ruby
require 'net/http'
require 'json'
require 'uri'
deck = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
# ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
uri = URI("https://api.example.com/public/meta?deck=#{URI.encode_www_form_component(deck)}")
meta = JSON.parse(Net::HTTP.get(uri))
puts "#{meta['deck_class']} / #{meta['dust_cost']} ΠΏΡΠ»ΠΈ"
# PNG
img = Net::HTTP.get(URI("https://api.example.com/public/render?deck=#{URI.encode_www_form_component(deck)}"))
File.binwrite("deck.png", img)π£ C# / .NET
using System.Net.Http;
using System.Text.Json;
var deck = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
using var http = new HttpClient { BaseAddress = new Uri("https://api.example.com") };
// ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
using var meta = await http.GetStreamAsync($"/public/meta?deck={Uri.EscapeDataString(deck)}");
using var doc = await JsonDocument.ParseAsync(meta);
Console.WriteLine(doc.RootElement.GetProperty("deck_class").GetString());
// PNG
var png = await http.GetByteArrayAsync($"/public/render?deck={Uri.EscapeDataString(deck)}");
await File.WriteAllBytesAsync("deck.png", png);π¦ Rust (reqwest)
use serde::Deserialize;
#[derive(Deserialize)]
struct DeckMeta {
deck_class: String,
dust_cost: u32,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let deck = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
let client = reqwest::Client::new();
let meta: DeckMeta = client
.get("https://api.example.com/public/meta")
.query(&[("deck", deck)])
.send().await?
.json().await?;
println!("{} / {} ΠΏΡΠ»ΠΈ", meta.deck_class, meta.dust_cost);
let png = client
.get("https://api.example.com/public/render")
.query(&[("deck", deck)])
.send().await?
.bytes().await?;
std::fs::write("deck.png", &png)?;
Ok(())
}β Java (HttpClient)
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
var deck = URLEncoder.encode("AAECAa0GBsubBOWwBI...", java.nio.charset.StandardCharsets.UTF_8);
var http = HttpClient.newHttpClient();
// ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅
var meta = http.send(
HttpRequest.newBuilder(URI.create("https://api.example.com/public/meta?deck=" + deck)).build(),
HttpResponse.BodyHandlers.ofString()
).body();
System.out.println(meta);
// PNG
http.send(
HttpRequest.newBuilder(URI.create("https://api.example.com/public/render?deck=" + deck)).build(),
HttpResponse.BodyHandlers.ofFile(Path.of("deck.png"))
);π cURL / Bash
DECK="AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA"
BASE="https://api.example.com"
# ΠΠ΅ΡΠ°Π΄Π°Π½Π½ΡΠ΅ β ΠΊΡΠ°ΡΠΈΠ²ΡΠΉ JSON ΡΠ΅ΡΠ΅Π· jq
curl -sS "$BASE/public/meta?deck=$(printf %s "$DECK" | jq -sRr @uri)" | jq .
# Π‘ΠΎΡ
ΡΠ°Π½ΠΈΡΡ PNG
curl -sS "$BASE/public/render?deck=$(printf %s "$DECK" | jq -sRr @uri)" -o deck.pngβ‘ Frameworks (React, Vue, Svelte, Next.js)
React (hook):
import { useEffect, useState } from 'react';
export function useDeckMeta(deckCode) {
const [meta, setMeta] = useState(null);
useEffect(() => {
if (!deckCode) return;
fetch(`https://api.example.com/public/meta?deck=${encodeURIComponent(deckCode)}`)
.then(r => r.json()).then(setMeta);
}, [deckCode]);
return meta;
}Vue 3 Composition API:
<script setup>
import { ref, watchEffect } from 'vue';
const props = defineProps(['code']);
const meta = ref(null);
watchEffect(async () => {
if (!props.code) return;
meta.value = await fetch(`https://api.example.com/public/meta?deck=${encodeURIComponent(props.code)}`).then(r => r.json());
});
</script>Next.js Server Component:
export default async function DeckCard({ code }) {
const meta = await fetch(
`https://api.example.com/public/meta?deck=${encodeURIComponent(code)}`,
{ next: { revalidate: 3600 } }
).then(r => r.json());
return <p>{meta.deck_class} β {meta.dust_cost} ΠΏΡΠ»ΠΈ</p>;
}π‘ Π‘ΠΎΠ²Π΅Ρ: Π΄Π»Ρ production-ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ Π²ΡΠ΅Π³Π΄Π° ΡΠΊΠ°Π·ΡΠ²Π°ΠΉΡΠ΅
timeoutΠΈ ΠΊΡΡΠΈΡΡΠΉΡΠ΅ ΠΎΡΠ²Π΅ΡΡ. ΠΠ½Π΄ΠΏΠΎΠΈΠ½ΡΡ/public/metaΠΈ/public/renderΠ΄Π°ΡΡ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡΠΉ ΡΠ΅Π·ΡΠ»ΡΡΠ°Ρ Π½Π° ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ β ΠΎΡΠ»ΠΈΡΠ½ΠΎ ΠΏΠΎΠ΄Ρ ΠΎΠ΄ΡΡ Π΄Π»Ρ CDN-ΠΊΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ (Cache-Control: public, max-age=86400).
API ΡΠΎΠ²ΠΌΠ΅ΡΡΠΈΠΌ Ρ Π»ΡΠ±ΡΠΌΠΈ WordPress-ΠΏΠ»Π°Π³ΠΈΠ½Π°ΠΌΠΈ, Π΄ΡΡΠ³Π°ΡΡΠΈΠΌΠΈ Π²Π½Π΅ΡΠ½ΠΈΠ΅ REST-ΡΠ΅ΡΠ²ΠΈΡΡ ΠΈΠ· Π±ΡΠ°ΡΠ·Π΅ΡΠ°: WPGetAPI, WP Webhooks, JetEngine REST Listing, Bricks Builder Query Loop, Elementor Dynamic Tags, Custom JS/HTML widgets ΠΈ Ρ.Π΄.
Π ΠΊΠΎΡΠ½Π΅ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ Π½Π°ΡΡΡΠΎΠ΅Π½ CORSMiddleware. Π£ΠΏΡΠ°Π²Π»ΡΠ΅ΡΡΡ ΡΠ΅ΡΠ΅Π· .env:
# Π Π°Π·ΡΠ΅ΡΠΈΡΡ Π²ΡΠ΅ΠΌ (ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ, ΡΠ΄ΠΎΠ±Π½ΠΎ Π΄Π»Ρ ΠΏΡΠ±Π»ΠΈΡΠ½ΠΎΠ³ΠΎ API)
CORS_ALLOW_ORIGINS=*
# ΠΠ»ΠΈ ΠΎΠ³ΡΠ°Π½ΠΈΡΠΈΡΡ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½ΡΠΌΠΈ Π΄ΠΎΠΌΠ΅Π½Π°ΠΌΠΈ WP-ΡΠ°ΠΉΡΠ°
CORS_ALLOW_ORIGINS=https://hs-manacost.ru,https://www.hs-manacost.ruΠ Π°Π·ΡΠ΅ΡΡΠ½Π½ΡΠ΅ ΠΌΠ΅ΡΠΎΠ΄Ρ: GET, POST, OPTIONS. ΠΠ°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ Content-Type, X-API-Key, Authorization ΠΏΡΠΎΠΏΡΡΠΊΠ°ΡΡΡΡ. Preflight-ΠΊΡΡ Π½Π° 1 ΡΠ°Ρ.
ΠΠΎΠ±Π°Π²ΡΡΠ΅ Π² functions.php ΡΠ΅ΠΌΡ:
function manacost_deck_shortcode($atts) {
$atts = shortcode_atts(['deck' => '', 'alt' => 'Hearthstone deck'], $atts);
if (empty($atts['deck'])) return '';
$url = 'https://api.example.com/public/render?deck=' . urlencode($atts['deck']);
return sprintf(
'<img src="%s" alt="%s" loading="lazy" style="max-width:100%%;height:auto" />',
esc_url($url),
esc_attr($atts['alt'])
);
}
add_shortcode('hs_deck', 'manacost_deck_shortcode');ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ Π² Π»ΡΠ±ΠΎΠΌ ΡΠ΅Π΄Π°ΠΊΡΠΎΡΠ΅:
[hs_deck deck="AAECAa0GBsubBOWwBI..." alt="ΠΠΎΠ½ΡΡΠΎΠ»Ρ ΠΠΎΠΈΠ½"]
Π§ΡΠΎΠ±Ρ ΠΊΠ»ΠΈΠ΅Π½Ρ ΡΡΡΡΠ°Π»ΡΡ Π² ΡΠ²ΠΎΠΉ ΠΆΠ΅ Π΄ΠΎΠΌΠ΅Π½ (Π½ΠΈΠΊΠ°ΠΊΠΈΡ CORS-ΠΏΡΠΎΠ±Π»Π΅ΠΌ Π΄Π°ΠΆΠ΅ Ρ ΡΠ°ΠΌΡΡ ΡΡΠ°ΡΡΡ Π±ΡΠ°ΡΠ·Π΅ΡΠΎΠ²):
add_action('rest_api_init', function () {
register_rest_route('manacost/v1', '/deck/(?P<code>[A-Za-z0-9+/=]+)', [
'methods' => 'GET',
'callback' => function ($req) {
$code = $req['code'];
$resp = wp_remote_get("https://api.example.com/public/meta?deck=" . rawurlencode($code), ['timeout' => 15]);
if (is_wp_error($resp)) return new WP_Error('upstream', $resp->get_error_message(), ['status' => 502]);
return new WP_REST_Response(json_decode(wp_remote_retrieve_body($resp), true), wp_remote_retrieve_response_code($resp));
},
'permission_callback' => '__return_true',
]);
});ΠΠΎΡΠ»Π΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: GET /wp-json/manacost/v1/deck/AAECAa0G...
<div id="deck-meta"></div>
<script>
(async () => {
const code = "AAECAa0GBsubBOWwBIWfBYGhBaChBbyhBQyY6wOtigSJowSktgShtgSHtwTbuQT++QT9+wSUoQX9ogW8owUA";
const res = await fetch(`https://api.example.com/public/meta?deck=${encodeURIComponent(code)}`);
if (!res.ok) return;
const meta = await res.json();
document.getElementById('deck-meta').innerHTML = `
<p>ΠΠ»Π°ΡΡ: <b>${meta.deck_class}</b></p>
<p>Π€ΠΎΡΠΌΠ°Ρ: <b>${meta.deck_format}</b></p>
<p>ΠΡΠ»Ρ: <b>${meta.dust_cost}</b></p>`;
})();
</script>| ΠΠΎΠ»Π΅ | ΠΠ½Π°ΡΠ΅Π½ΠΈΠ΅ |
|---|---|
| API URL | https://api.example.com |
| Endpoint | /public/meta |
| Method | GET |
| Query parameter | deck = {{shortcode_arg:deck}} |
| Headers | (Π½Π΅ Π½ΡΠΆΠ½Ρ) |
| Output | [wpgetapi_endpoint api_id="manacost" endpoint_id="meta" debug="0"] |
β οΈ ΠΡΠ»ΠΈ API Π·Π°ΠΊΡΡΡ API-ΠΊΠ»ΡΡΠΎΠΌ β Π΄ΠΎΠ±Π°Π²ΡΡΠ΅ headerX-API-Key: <Π²Π°Ρ ΠΊΠ»ΡΡ>Π² Π½Π°ΡΡΡΠΎΠΉΠΊΠ°Ρ ΠΏΠ»Π°Π³ΠΈΠ½Π°. CORS ΡΠΆΠ΅ ΡΠ°Π·ΡΠ΅ΡΠ°Π΅Ρ ΡΡΠΎΡ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ.
ΠΠΎΡ ΡΠ°ΠΌ ΡΠΌΠ΅Π΅Ρ ΡΠΎΠ·Π΄Π°Π²Π°ΡΡ ΠΏΠΎΡΡΡ Ρ ΠΊΠΎΠ»ΠΎΠ΄Π°ΠΌΠΈ ΡΠ΅ΡΠ΅Π· WP REST API. ΠΠΊΠ»ΡΡΠ°Π΅ΡΡΡ ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΠΌΠΈ WP_BASE_URL, WP_USER, WP_APP_PASSWORD Π² .env β ΡΠΌ. Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΠΈ Π½Π°ΡΡΡΠΎΠΉΠΊΠ°. Application Password ΡΠΎΠ·Π΄Π°ΡΡΡΡ Π² Π°Π΄ΠΌΠΈΠ½ΠΊΠ΅ WP: ΠΡΠΎΡΠΈΠ»Ρ β Application Passwords.
| Π‘ΠΈΠΌΠΏΡΠΎΠΌ | Π§ΡΠΎ ΠΏΡΠΎΠ²Π΅ΡΠΈΡΡ |
|---|---|
ModuleNotFoundError: pkg_resources |
ΠΠ±Π½ΠΎΠ²ΠΈΡΠ΅ ΡΠ΅ΠΏΠΎΠ·ΠΈΡΠΎΡΠΈΠΉ β Π·Π°ΠΌΠ΅Π½Π΅Π½ΠΎ Π½Π° importlib.metadata. ΠΡΠ»ΠΈ ΠΊΠΎΠ΄ ΡΡΠ°ΡΡΠΉ: pip install "setuptools<81" |
BOT_TOKEN Π½Π΅ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½ ΠΏΡΠΈ Π·Π°ΠΏΡΡΠΊΠ΅ API |
Π€Π°ΠΉΠ» .env ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ Π² ΡΠ°Π±ΠΎΡΠ΅ΠΉ Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ ΠΈ ΡΠΎΠ΄Π΅ΡΠΆΠΈΡ BOT_TOKEN=... (Π·Π½Π°ΡΠ΅Π½ΠΈΠ΅ ΠΌΠΎΠΆΠ΅Ρ Π±ΡΡΡ Π»ΡΠ±ΡΠΌ, Π΅ΡΠ»ΠΈ Π±ΠΎΡ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΡΡΡ) |
/public/render Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ 400 Π½Π° Π²Π°Π»ΠΈΠ΄Π½ΡΠΉ ΠΊΠΎΠ΄ |
ΠΠ΅ Π·Π°Π³ΡΡΠΆΠ΅Π½Π° Π±Π°Π·Π° ΠΊΠ°ΡΡ. ΠΠ°ΠΏΡΡΡΠΈΡΠ΅ python update_cards.py ΠΈΠ»ΠΈ Π²ΠΊΠ»ΡΡΠΈΡΠ΅ Blizzard API |
ΠΠΎΠ³ΠΈ Π·Π°ΠΌΡΡΠΎΡΠ΅Π½Ρ binascii.Error traceback'Π°ΠΌΠΈ |
ΠΠΆΠΈΠ΄Π°Π΅ΠΌΠΎ ΠΏΡΠΈ Π½Π΅Π²Π°Π»ΠΈΠ΄Π½ΡΡ ΠΊΠΎΠ΄Π°Ρ ΠΎΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ΠΉ β API Π²ΡΡ ΡΠ°Π²Π½ΠΎ ΠΎΡΠ΄Π°ΡΡ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΡΠΉ 400 |
| ΠΠΎΡΡ 8000 Π·Π°Π½ΡΡ | uvicorn api:app --port 8001 ΠΈΠ»ΠΈ lsof -i:8000 ΡΡΠΎΠ±Ρ Π½Π°ΠΉΡΠΈ ΠΏΡΠΎΡΠ΅ΡΡ |
WP-ΠΏΠ»Π°Π³ΠΈΠ½ ΠΏΠΎΠ»ΡΡΠ°Π΅Ρ CORS error Π² ΠΊΠΎΠ½ΡΠΎΠ»ΠΈ |
ΠΡΠΎΠ²Π΅ΡΡΡΠ΅ CORS_ALLOW_ORIGINS Π² .env β Π΄ΠΎΠΌΠ΅Π½ ΡΠ°ΠΉΡΠ° Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ Π² ΡΠΏΠΈΡΠΊΠ΅ (ΠΈΠ»ΠΈ *). ΠΠ΅ Π·Π°Π±ΡΠ΄ΡΡΠ΅ ΠΏΠ΅ΡΠ΅Π·Π°ΠΏΡΡΡΠΈΡΡ uvicorn ΠΏΠΎΡΠ»Π΅ ΠΏΡΠ°Π²ΠΊΠΈ .env |
| Preflight (OPTIONS) Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ 405 | Π‘ΡΠ°ΡΠ°Ρ Π²Π΅ΡΡΠΈΡ api.py Π±Π΅Π· CORSMiddleware β ΠΎΠ±Π½ΠΎΠ²ΠΈΡΠ΅ ΡΠ΅ΠΏΠΎΠ·ΠΈΡΠΎΡΠΈΠΉ Π΄ΠΎ ΠΏΠΎΡΠ»Π΅Π΄Π½Π΅Π³ΠΎ ΠΊΠΎΠΌΠΌΠΈΡΠ° |
tg-manacost-bot/
βββ bot.py # ΠΡΠ½ΠΎΠ²Π½ΠΎΠΉ ΡΠ°ΠΉΠ» Telegram-Π±ΠΎΡΠ°
βββ api.py # FastAPI HTTP API
βββ config.py # ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΊΠΎΠ½ΡΠΈΠ³ΡΡΠ°ΡΠΈΠΈ ΠΈΠ· .env
βββ loader.py # ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΈ ΠΏΠ°ΡΡΠΈΠ½Π³ Π±Π°Π·Ρ ΠΊΠ°ΡΡ
βββ generator.py # ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄
βββ database.py # SQLite Π±Π°Π·Π° Π΄Π°Π½Π½ΡΡ
(ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠ°, Π³ΠΎΠ»ΠΎΡΠ°)
βββ hsguru_scraper.py # ΠΠ²ΡΠΎΠΏΠ°ΡΡΠ΅Ρ ΠΊΠΎΠ»ΠΎΠ΄ Ρ hsguru.com
βββ wordpress.py # ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ Ρ WordPress REST API
βββ blizzard_api.py # ΠΠ»ΠΈΠ΅Π½Ρ Blizzard Hearthstone API
βββ update_cards.py # Π‘ΠΊΡΠΈΠΏΡ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ Π±Π°Π·Ρ ΠΊΠ°ΡΡ
βββ ΠΡΡ
Π΅ΡΠΈΠΏΡ.csv # Π’Π°Π±Π»ΠΈΡΠ° ΠΏΠ΅ΡΠ΅Π²ΠΎΠ΄ΠΎΠ² Π°ΡΡ
Π΅ΡΠΈΠΏΠΎΠ² (EN β RU)
βββ requirements.txt # ΠΠ°Π²ΠΈΡΠΈΠΌΠΎΡΡΠΈ Python
βββ cards/ # PNG-ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΊΠ°ΡΡ
βββ cache/ # ΠΡΡ (ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΊΠΎΠ»ΠΎΠ΄, API-ΠΎΡΠ²Π΅ΡΡ, seen)
βββ templates/ # HTML-ΡΠ°Π±Π»ΠΎΠ½Ρ (admin panel)
| ΠΠΎΠΌΠ°Π½Π΄Π° | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|
/start |
ΠΡΠΈΠ²Π΅ΡΡΡΠ²Π΅Π½Π½ΠΎΠ΅ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ |
/help |
Π‘ΠΏΡΠ°Π²ΠΊΠ° ΠΏΠΎ ΠΊΠΎΠΌΠ°Π½Π΄Π°ΠΌ |
/image <Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°ΡΡΡ> |
ΠΠΎΠΊΠ°Π·Π°ΡΡ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΊΠ°ΡΡΡ |
/search_deck <Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠ°ΡΡΡ> |
ΠΠ°ΠΉΡΠΈ ΠΊΠΎΠ»ΠΎΠ΄Ρ Ρ ΡΡΠΎΠΉ ΠΊΠ°ΡΡΠΎΠΉ |
/wp <ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ> |
ΠΠ°Π³ΡΡΠ·ΠΈΡΡ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ Π² WordPress |
ΠΠ»Ρ Π°Π΄ΠΌΠΈΠ½ΠΈΡΡΡΠ°ΡΠΎΡΠΎΠ²:
| ΠΠΎΠΌΠ°Π½Π΄Π° | ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ |
|---|---|
/admin |
ΠΡΠΊΡΡΡΡ ΠΏΠ°Π½Π΅Π»Ρ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ |
/post |
ΠΡΡΡΠ½ΡΡ ΠΎΠΏΡΠ±Π»ΠΈΠΊΠΎΠ²Π°ΡΡ ΠΎΠ΄Π½Ρ ΠΊΠΎΠ»ΠΎΠ΄Ρ Ρ HSGuru |
/force_publish |
ΠΡΠΈΠ½ΡΠ΄ΠΈΡΠ΅Π»ΡΠ½Π°Ρ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΡ (ΠΈ Π½Π° ΡΠ°ΠΉΡ, ΠΈ Π² ΠΊΠ°Π½Π°Π») |
- ΠΠ½ΡΠ΅ΡΠ²Π°Π»: 1 ΠΊΠΎΠ»ΠΎΠ΄Π° ΠΊΠ°ΠΆΠ΄ΡΠ΅ 30 ΠΌΠΈΠ½ΡΡ
- ΠΠΈΠ½ΠΈΠΌΡΠΌ ΠΈΠ³Ρ: 20 (ΠΊΠΎΠ»ΠΎΠ΄Ρ Ρ ΠΌΠ΅Π½ΡΡΠΈΠΌ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎΠΌ ΠΏΡΠΎΠΏΡΡΠΊΠ°ΡΡΡΡ)
- ΠΡΠ±Π»ΠΈΠΊΠ°ΡΡ: ΠΏΡΠΎΠ²Π΅ΡΡΡΡΡΡ ΠΏΠΎ ΡΡΡΠΌ ΠΊΡΠΈΡΠ΅ΡΠΈΡΠΌ:
- Π’ΠΎΡΠ½ΠΎΠ΅ ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° ΠΊΠΎΠ»ΠΎΠ΄Ρ
- Π‘Ρ ΠΎΠΆΠ΅ΡΡΡ Π½Π°Π±ΠΎΡΠ° ΠΊΠ°ΡΡ β₯ 90% (ΠΊΠΎΡΡΡΠΈΡΠΈΠ΅Π½Ρ ΠΠ°ΠΊΠΊΠ°ΡΠ°)
- Π‘ΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅ Π½Π°Π·Π²Π°Π½ΠΈΡ (ΠΊΡΠΎΠΌΠ΅ generic-ΠΈΠΌΡΠ½: Paladin, Mage, Warrior, Demon Hunter, Death Knight, Shaman ΠΈ Ρ.Π΄.)
- Wild-ΡΠΈΠ»ΡΡΡ: Π½Π΅ Π±ΠΎΠ»Π΅Π΅ ΠΎΠ΄Π½ΠΎΠΉ ΠΠΎΠ»ΡΠ½ΠΎΠΉ ΠΊΠΎΠ»ΠΎΠ΄Ρ ΠΏΠΎΠ΄ΡΡΠ΄
- ΠΠ½ΡΠ΅ΡΠ²Π°Π»: Π½Π΅ ΡΠ°ΡΠ΅ 1 ΡΠ°Π·Π° Π² 2 ΡΠ°ΡΠ°
- ΠΡΠ±Π»ΠΈΠΊΡΠ΅ΡΡΡ ΡΠ° ΠΆΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Π°, ΡΡΠΎ ΠΈ Π½Π° ΡΠ°ΠΉΡ (Π΅ΡΠ»ΠΈ ΠΏΡΠΎΡΠ»ΠΎ β₯ 2 ΡΠ°ΡΠΎΠ²)
ΠΡΠΎΠ΅ΠΊΡ ΡΠ°ΡΠΏΡΠΎΡΡΡΠ°Π½ΡΠ΅ΡΡΡ ΠΏΠΎΠ΄ Π»ΠΈΡΠ΅Π½Π·ΠΈΠ΅ΠΉ MIT β ΡΠ²ΠΎΠ±ΠΎΠ΄Π½ΠΎ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅, ΠΈΠ·ΠΌΠ΅Π½ΡΠΉΡΠ΅ ΠΈ ΡΠ°ΡΠΏΡΠΎΡΡΡΠ°Π½ΡΠΉΡΠ΅ Ρ ΡΠΊΠ°Π·Π°Π½ΠΈΠ΅ΠΌ Π°Π²ΡΠΎΡΡΡΠ²Π°.
Π‘Π΄Π΅Π»Π°Π½ΠΎ Ρ β€οΈ Π΄Π»Ρ ΡΠΎΠΎΠ±ΡΠ΅ΡΡΠ²Π° Hearthstone
ΠΡΠ»ΠΈ ΠΏΡΠΎΠ΅ΠΊΡ ΠΏΠΎΠ»Π΅Π·Π΅Π½ β ΠΏΠΎΡΡΠ°Π²ΡΡΠ΅ β Π½Π° GitHub