Skip to content

Zulut30/deckview-telegram-bot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸƒ Hearthstone Deck Visualizer

Telegram-Π±ΠΎΡ‚, HTTP API ΠΈ Π°Π²Ρ‚ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΊΠΎΠ»ΠΎΠ΄ Hearthstone.
ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅Ρ‚ ΠΊΠΎΠ΄Ρ‹ ΠΊΠΎΠ»ΠΎΠ΄ Π² сообщСниях, Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ красивыС изобраТСния ΠΈ автоматичСски ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠ΅Ρ‚ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹ стримСров Π½Π° сайт ΠΈ Π² Telegram-ΠΊΠ°Π½Π°Π».

Website Telegram channel Python FastAPI License Status


✨ ВозмоТности

  • πŸ–Ό ГСнСрация ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄ β€” высококачСствСнный Ρ€Π΅Π½Π΄Π΅Ρ€ ΠΏΠΎ ΠΊΠΎΠ΄Ρƒ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹ 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

ΠŸΡƒΠ±Π»ΠΈΡ‡Π½ΠΎΠ΅ API доступно Π±Π΅Π· Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΠΎ адрСсу сСрвСра. ВсС ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹Π΅ endpoints ΠΈΠΌΠ΅ΡŽΡ‚ прСфикс /public/.

Π˜Π½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½Π°Ρ докумСнтация Swagger UI доступна ΠΏΠΎ адрСсу:

https://your-server/docs

1. ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹

Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ 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 НСвСрный ΠΈΠ»ΠΈ Π½Π΅ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹ΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹

2. ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹

Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ 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 НСвСрный ΠΈΠ»ΠΈ Π½Π΅ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹ΠΉ ΠΊΠΎΠ΄ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹

3. Бписок ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΎΠ² Π°Ρ€Ρ…Π΅Ρ‚ΠΈΠΏΠΎΠ²

Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΏΠΎΠ»Π½ΡƒΡŽ Ρ‚Π°Π±Π»ΠΈΡ†Ρƒ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΎΠ² Π½Π°Π·Π²Π°Π½ΠΈΠΉ ΠΊΠΎΠ»ΠΎΠ΄ с английского Π½Π° русский.

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 РусскоС Π½Π°Π·Π²Π°Π½ΠΈΠ΅ Π°Ρ€Ρ…Π΅Ρ‚ΠΈΠΏΠ°

4. ΠŸΠ΅Ρ€Π΅Π²Π΅ΡΡ‚ΠΈ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹

ΠŸΠ΅Ρ€Π΅Π²ΠΎΠ΄ΠΈΡ‚ ΠΎΠ΄Π½ΠΎ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹ с английского Π½Π° русский. Если Ρ‚ΠΎΡ‡Π½ΠΎΠ³ΠΎ ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° Π½Π΅Ρ‚ β€” Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π».

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-Π±ΠΎΡ‚Π°)

1. ΠšΠ»ΠΎΠ½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ рСпозитория

git clone https://github.com/Zulut30/deckview-telegram-bot.git
cd deckview-telegram-bot

2. Установка зависимостСй

РСкомСндуСтся ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ΅ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅:

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) β€” Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ зависимости Π΅Π³ΠΎ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚.

3. Настройка окруТСния

Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» .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=24

Запуск

Запуск Telegram-Π±ΠΎΡ‚Π°

python bot.py

Запуск HTTP API (ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎ ΠΎΡ‚ Π±ΠΎΡ‚Π°)

uvicorn 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.

Быстрая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° API (smoke test)

# 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

ΠŸΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡ API Π½Π°Ρ€ΡƒΠΆΡƒ (reverse proxy)

НС ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠΉΡ‚Π΅ 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;
    }
}

Запуск Π² ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Π΅ Ρ‡Π΅Ρ€Π΅Π· systemd

Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ /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.target
sudo systemctl daemon-reload
sudo systemctl enable --now deckview-bot deckview-api
sudo systemctl status deckview-bot deckview-api

ОбновлСниС Π±Π°Π·Ρ‹ ΠΊΠ°Ρ€Ρ‚

python update_cards.py

🌍 ΠšΠ»ΠΈΠ΅Π½Ρ‚Ρ‹ Π½Π° Ρ€Π°Π·Π½Ρ‹Ρ… языках

API β€” это ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ 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).


πŸ”Œ Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с WordPress

API совмСстим с Π»ΡŽΠ±Ρ‹ΠΌΠΈ WordPress-ΠΏΠ»Π°Π³ΠΈΠ½Π°ΠΌΠΈ, Π΄Ρ‘Ρ€Π³Π°ΡŽΡ‰ΠΈΠΌΠΈ внСшниС REST-сСрвисы ΠΈΠ· Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°: WPGetAPI, WP Webhooks, JetEngine REST Listing, Bricks Builder Query Loop, Elementor Dynamic Tags, Custom JS/HTML widgets ΠΈ Ρ‚.Π΄.

CORS

Π’ ΠΊΠΎΡ€Π½Π΅ прилоТСния настроСн 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 час.

Π’Π°Ρ€ΠΈΠ°Π½Ρ‚ 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="ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ Π’ΠΎΠΈΠ½"]

Π’Π°Ρ€ΠΈΠ°Π½Ρ‚ 2 β€” REST-прокси Π½Π° сторонС WordPress

Π§Ρ‚ΠΎΠ±Ρ‹ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ стучался Π² свой ΠΆΠ΅ Π΄ΠΎΠΌΠ΅Π½ (Π½ΠΈΠΊΠ°ΠΊΠΈΡ… 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...

Π’Π°Ρ€ΠΈΠ°Π½Ρ‚ 3 β€” клиСнтский JS (Elementor / Bricks / любой HTML-Π±Π»ΠΎΠΊ)

<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>

Π’Π°Ρ€ΠΈΠ°Π½Ρ‚ 4 β€” конфигурация WPGetAPI

ПолС Π—Π½Π°Ρ‡Π΅Π½ΠΈΠ΅
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-ΠΊΠ»ΡŽΡ‡ΠΎΠΌ β€” Π΄ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ header X-API-Key: <ваш ΠΊΠ»ΡŽΡ‡> Π² настройках ΠΏΠ»Π°Π³ΠΈΠ½Π°. CORS ΡƒΠΆΠ΅ Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅Ρ‚ этот Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ.

Автопубликация постов Π² WordPress

Π‘ΠΎΡ‚ сам ΡƒΠΌΠ΅Π΅Ρ‚ ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ посты с ΠΊΠΎΠ»ΠΎΠ΄Π°ΠΌΠΈ Ρ‡Π΅Ρ€Π΅Π· 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 ΠŸΡ€ΠΈΠ½ΡƒΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ публикация (ΠΈ Π½Π° сайт, ΠΈ Π² ΠΊΠ°Π½Π°Π»)

ΠŸΡ€Π°Π²ΠΈΠ»Π° ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΊΠΎΠ»ΠΎΠ΄

На сайт WordPress

  • Π˜Π½Ρ‚Π΅Ρ€Π²Π°Π»: 1 ΠΊΠΎΠ»ΠΎΠ΄Π° ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 30 ΠΌΠΈΠ½ΡƒΡ‚
  • ΠœΠΈΠ½ΠΈΠΌΡƒΠΌ ΠΈΠ³Ρ€: 20 (ΠΊΠΎΠ»ΠΎΠ΄Ρ‹ с мСньшим количСством ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ)
  • Π”ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Ρ‹: ΠΏΡ€ΠΎΠ²Π΅Ρ€ΡΡŽΡ‚ΡΡ ΠΏΠΎ Ρ‚Ρ€Ρ‘ΠΌ критСриям:
    1. Π’ΠΎΡ‡Π½ΠΎΠ΅ совпадСниС ΠΊΠΎΠ΄Π° ΠΊΠΎΠ»ΠΎΠ΄Ρ‹
    2. Π‘Ρ…ΠΎΠΆΠ΅ΡΡ‚ΡŒ Π½Π°Π±ΠΎΡ€Π° ΠΊΠ°Ρ€Ρ‚ β‰₯ 90% (коэффициСнт Π–Π°ΠΊΠΊΠ°Ρ€Π°)
    3. Π‘ΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΠ΅ названия (ΠΊΡ€ΠΎΠΌΠ΅ generic-ΠΈΠΌΡ‘Π½: Paladin, Mage, Warrior, Demon Hunter, Death Knight, Shaman ΠΈ Ρ‚.Π΄.)
  • Wild-Ρ„ΠΈΠ»ΡŒΡ‚Ρ€: Π½Π΅ Π±ΠΎΠ»Π΅Π΅ ΠΎΠ΄Π½ΠΎΠΉ Π’ΠΎΠ»ΡŒΠ½ΠΎΠΉ ΠΊΠΎΠ»ΠΎΠ΄Ρ‹ подряд

Π’ Telegram-ΠΊΠ°Π½Π°Π»

  • Π˜Π½Ρ‚Π΅Ρ€Π²Π°Π»: Π½Π΅ Ρ‡Π°Ρ‰Π΅ 1 Ρ€Π°Π·Π° Π² 2 часа
  • ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠ΅Ρ‚ΡΡ Ρ‚Π° ΠΆΠ΅ ΠΊΠΎΠ»ΠΎΠ΄Π°, Ρ‡Ρ‚ΠΎ ΠΈ Π½Π° сайт (Ссли ΠΏΡ€ΠΎΡˆΠ»ΠΎ β‰₯ 2 часов)

ЛицСнзия

ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ распространяСтся ΠΏΠΎΠ΄ Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ MIT β€” свободно ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅, измСняйтС ΠΈ распространяйтС с ΡƒΠΊΠ°Π·Π°Π½ΠΈΠ΅ΠΌ авторства.


Π‘Π΄Π΅Π»Π°Π½ΠΎ с ❀️ для сообщСства Hearthstone
Если ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ ΠΏΠΎΠ»Π΅Π·Π΅Π½ β€” ΠΏΠΎΡΡ‚Π°Π²ΡŒΡ‚Π΅ ⭐ Π½Π° GitHub

About

πŸƒ Telegram-Π±ΠΎΡ‚ ΠΈ HTTP API для Π²ΠΈΠ·ΡƒΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΊΠΎΠ»ΠΎΠ΄ Hearthstone. Авто-распознаваниС ΠΊΠΎΠ΄ΠΎΠ², Ρ€Π΅Π½Π΄Π΅Ρ€ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ, публикация Π½Π° сайт ΠΈ Π² ΠΊΠ°Π½Π°Π».

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors