mirror of
https://github.com/Redume/Shirino.git
synced 2024-11-06 00:33:59 +03:00
commit
de3b3a393b
7 changed files with 383 additions and 70 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1 +1,8 @@
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
.mypy_cache
|
||||||
|
|
||||||
|
venv
|
||||||
|
.env
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2023 Redume
|
Copyright (c) 2023 Redume, DarkCat09 (Chechkenev Andrey)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Shirino
|
||||||
|
|
||||||
|
## Что это?
|
||||||
|
Бот для телеграма. Выводит курс валют.
|
||||||
|
https://t.me/Shirino_bot
|
||||||
|
|
||||||
|
## Хочу запустить
|
||||||
|
Получите токен бота в телеграме и токен CoinAPI.
|
||||||
|
Вставьте в файл `.env` в формате:
|
||||||
|
```
|
||||||
|
COINAPI_KEY=тут для койнапи
|
||||||
|
TELEGRAM_TOKEN=тут для телеграма
|
||||||
|
```
|
||||||
|
|
||||||
|
В энв файл ещё можно такие переменные добавить:
|
||||||
|
```
|
||||||
|
DEBUG=false или true, включает отладочные логи
|
||||||
|
TIMEOUT=таймаут для библиотеки requests, в секундах (2 по дефолту)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Хочу сделать пулл-реквест
|
||||||
|
Ставьте pylint и mypy для статической проверки кода.
|
||||||
|
Конфиги уже есть в репозитории.
|
||||||
|
После проверок можете открывать PR.
|
217
main.py
217
main.py
|
@ -1,103 +1,184 @@
|
||||||
import requests
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from aiogram import Bot, types
|
import logging
|
||||||
from aiogram.dispatcher import Dispatcher
|
from typing import Any, Dict, List, Optional
|
||||||
from aiogram.utils import executor
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
from aiogram import Bot # type: ignore
|
||||||
|
from aiogram.dispatcher import Dispatcher # type: ignore
|
||||||
|
from aiogram.utils import executor # type: ignore
|
||||||
|
|
||||||
|
from aiogram.types import InlineQuery # type: ignore
|
||||||
|
from aiogram.types import InlineQueryResultArticle
|
||||||
|
from aiogram.types import InputTextMessageContent
|
||||||
|
|
||||||
|
|
||||||
bot = Bot(token="5193950006:AAGU8elNfNB9FocVSIb4DnqoEvQk70Mqh5E")
|
# Constants
|
||||||
|
DDG_URL = 'https://duckduckgo.com/js/spice/currency'
|
||||||
|
COINAPI_URL = 'https://rest.coinapi.io/v1/exchangerate'
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
# Config from .env
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
debug: bool
|
||||||
|
timeout: int = 2
|
||||||
|
coinapi_key: str
|
||||||
|
telegram_token: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = '.env'
|
||||||
|
env_file_encoding = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings() # type: ignore
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log = logging.getLogger('shirino')
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||||
|
|
||||||
|
handler.setFormatter(fmt)
|
||||||
|
log.addHandler(handler)
|
||||||
|
|
||||||
|
if settings.debug:
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
# ---
|
||||||
|
|
||||||
|
|
||||||
|
bot = Bot(token=settings.telegram_token)
|
||||||
dp = Dispatcher(bot)
|
dp = Dispatcher(bot)
|
||||||
|
|
||||||
|
|
||||||
class TypeDict:
|
class CurrencyConverter:
|
||||||
def __init__(self):
|
|
||||||
self.amount = None
|
|
||||||
self.from_amount = None
|
|
||||||
self.from_currency = None
|
|
||||||
self.to_currency = None
|
|
||||||
|
|
||||||
@staticmethod
|
def __init__(self) -> None:
|
||||||
def get_currency(self, amount, from_currency, to_currency):
|
|
||||||
if amount is None:
|
|
||||||
amount = "1"
|
|
||||||
|
|
||||||
try:
|
self.amount = 1.0
|
||||||
page = requests.get(f"https://duckduckgo.com/js/spice/currency/{amount}/{from_currency}/{to_currency}")
|
self.conv_amount = 0.0
|
||||||
page = page.text.replace('ddg_spice_currency(', "").replace(');', "")
|
self.from_currency = ''
|
||||||
page = json.loads(page)
|
self.conv_currency = ''
|
||||||
|
|
||||||
if page["headers"]["description"].find("ERROR") != -1:
|
def convert(self) -> None:
|
||||||
print(from_currency, to_currency)
|
"""Currency conversion"""
|
||||||
crypto = requests.get(f"https://rest.coinapi.io/v1/exchangerate/{from_currency.upper()}/{to_currency.upper()}", headers={
|
|
||||||
"X-CoinAPI-Key": "8A465920-C233-4EE2-860B-A0AF9EC21FFF"
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
print(crypto)
|
if not self.ddgapi():
|
||||||
|
self.coinapi()
|
||||||
|
|
||||||
self.from_amount = crypto.get("rate")
|
def ddgapi(self) -> bool:
|
||||||
return crypto.get("rate")
|
"""Get data from DuckDuckGo's currency API
|
||||||
|
|
||||||
except KeyError:
|
Returns:
|
||||||
print("blyat slomal")
|
`False` if the currency does not exist,
|
||||||
return None
|
`True` on successful conversion
|
||||||
|
"""
|
||||||
|
|
||||||
return page.get("conversion")
|
# API request
|
||||||
|
resp = requests.get(
|
||||||
|
(
|
||||||
|
f'{DDG_URL}/{self.amount}/{self.from_currency}'
|
||||||
|
f'/{self.conv_currency}'
|
||||||
|
),
|
||||||
|
timeout=settings.timeout,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
log.debug(resp.text)
|
||||||
def is_num(value):
|
|
||||||
return value.isdecimal() or value.replace('.', '', 1).isdecimal()
|
|
||||||
|
|
||||||
|
# Parsing JSON data
|
||||||
|
data: Dict[str, Any] = json.loads(
|
||||||
|
resp.text
|
||||||
|
.replace('ddg_spice_currency(', '')
|
||||||
|
.replace(');', '')
|
||||||
|
)
|
||||||
|
|
||||||
type_dict = TypeDict()
|
log.debug(data)
|
||||||
|
|
||||||
|
# If the currency does not exist
|
||||||
|
descr = data.get('headers', {}).get('description', '')
|
||||||
|
if descr.find('ERROR') != -1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Otherwise
|
||||||
|
conv: Dict[str, str] = data.get('conversion', {})
|
||||||
|
self.conv_amount = float(conv.get('converted-amount', 0))
|
||||||
|
|
||||||
|
log.debug(conv)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def coinapi(self) -> None:
|
||||||
|
"""Get data from CoinAPI (for cryptocurrencies)"""
|
||||||
|
|
||||||
|
resp = requests.get(
|
||||||
|
(
|
||||||
|
f'{COINAPI_URL}/{self.from_currency}'
|
||||||
|
f'/{self.conv_currency}'
|
||||||
|
),
|
||||||
|
headers={
|
||||||
|
'X-CoinAPI-Key': settings.coinapi_key,
|
||||||
|
},
|
||||||
|
timeout=settings.timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
data: Dict[str, Any] = resp.json()
|
||||||
|
self.conv_amount = float(data.get('rate', 0))
|
||||||
|
|
||||||
|
|
||||||
@dp.inline_handler()
|
@dp.inline_handler()
|
||||||
async def currency(query: types.InlineQuery):
|
async def currency(inline_query: InlineQuery) -> None:
|
||||||
text = query.query.split(" ")
|
|
||||||
result_id: str = hashlib.md5(query.query.encode()).hexdigest()
|
|
||||||
|
|
||||||
if text == ['']:
|
query = inline_query.query
|
||||||
return
|
article: List[Optional[InlineQueryResultArticle]] = [None]
|
||||||
|
|
||||||
for i in range(len(text)):
|
text = query.split()
|
||||||
if type_dict.is_num(text[i]):
|
len_ = len(text)
|
||||||
continue
|
|
||||||
|
|
||||||
if text[i].find(",") != -1:
|
result_id = hashlib.md5(query.encode()).hexdigest()
|
||||||
text[i] = text[i].replace(",", ".")
|
conv = CurrencyConverter()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if type_dict.is_num(text[0]):
|
if len_ == 3:
|
||||||
res, crypto_rate = type_dict.get_currency(text[0], text[1], text[2])
|
conv.amount = float(text[0])
|
||||||
|
conv.from_currency = text[1].upper()
|
||||||
|
conv.conv_currency = text[2].upper()
|
||||||
|
conv.convert()
|
||||||
|
elif len_ == 2:
|
||||||
|
conv.from_currency = text[0].upper()
|
||||||
|
conv.conv_currency = text[1].upper()
|
||||||
|
conv.convert()
|
||||||
else:
|
else:
|
||||||
res, crypto_rate = type_dict.get_currency(None, text[0], text[1])
|
raise ValueError('Надо 2 или 3 аргумента')
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
if res is None:
|
result = (
|
||||||
return
|
f'{conv.amount} {conv.from_currency} = '
|
||||||
|
f'{conv.conv_amount} {conv.conv_currency}'
|
||||||
|
)
|
||||||
|
|
||||||
print(res)
|
except Exception as ex:
|
||||||
|
result = f'{type(ex).__name__}: {ex}'
|
||||||
|
|
||||||
from_amount = res.get('from_amount', res['from-amount'])
|
article[0] = InlineQueryResultArticle(
|
||||||
from_currency_symbol = res.get('from_currency_symbol', res['from-currency-symbol'])
|
|
||||||
converted_amount = res.get('converted_amount', res['converted-amount'])
|
|
||||||
to_currency_symbol = res.get('to_currency_symbol', res['to-currency-symbol'])
|
|
||||||
|
|
||||||
result = f"{from_amount} {from_currency_symbol} = {converted_amount} {to_currency_symbol}"
|
|
||||||
|
|
||||||
if crypto_rate:
|
|
||||||
result += f" | Crypto Rate: {crypto_rate}"
|
|
||||||
|
|
||||||
article = [types.InlineQueryResultArticle(
|
|
||||||
id=result_id,
|
id=result_id,
|
||||||
title=result,
|
title=result,
|
||||||
input_message_content=types.InputTextMessageContent(
|
input_message_content=InputTextMessageContent(
|
||||||
message_text=result
|
message_text=result,
|
||||||
))]
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await inline_query.answer(
|
||||||
|
article,
|
||||||
|
cache_time=1,
|
||||||
|
is_personal=True,
|
||||||
|
)
|
||||||
|
|
||||||
await query.answer(article, cache_time=1, is_personal=True)
|
|
||||||
|
|
||||||
executor.start_polling(dp, skip_updates=True)
|
executor.start_polling(dp, skip_updates=True)
|
||||||
|
|
5
mypy.ini
Normal file
5
mypy.ini
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[mypy]
|
||||||
|
check_untyped_defs = True
|
||||||
|
warn_return_any = True
|
||||||
|
warn_unreachable = True
|
||||||
|
show_error_codes = True
|
193
pylintrc
Normal file
193
pylintrc
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
[MAIN]
|
||||||
|
analyse-fallback-blocks=no
|
||||||
|
clear-cache-post-run=no
|
||||||
|
extension-pkg-allow-list=
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
fail-on=
|
||||||
|
fail-under=10
|
||||||
|
ignore=CVS
|
||||||
|
ignore-paths=
|
||||||
|
ignore-patterns=^\.#
|
||||||
|
ignored-modules=
|
||||||
|
jobs=4
|
||||||
|
limit-inference-results=100
|
||||||
|
load-plugins=
|
||||||
|
persistent=yes
|
||||||
|
py-version=3.10
|
||||||
|
recursive=no
|
||||||
|
suggestion-mode=yes
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
argument-naming-style=snake_case
|
||||||
|
attr-naming-style=snake_case
|
||||||
|
bad-names=foo,
|
||||||
|
bar,
|
||||||
|
baz,
|
||||||
|
toto,
|
||||||
|
tutu,
|
||||||
|
tata
|
||||||
|
bad-names-rgxs=
|
||||||
|
class-attribute-naming-style=any
|
||||||
|
class-const-naming-style=UPPER_CASE
|
||||||
|
class-naming-style=PascalCase
|
||||||
|
const-naming-style=UPPER_CASE
|
||||||
|
docstring-min-length=-1
|
||||||
|
function-naming-style=snake_case
|
||||||
|
good-names=i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
ex,
|
||||||
|
Run,
|
||||||
|
_
|
||||||
|
good-names-rgxs=
|
||||||
|
include-naming-hint=no
|
||||||
|
inlinevar-naming-style=any
|
||||||
|
method-naming-style=snake_case
|
||||||
|
module-naming-style=snake_case
|
||||||
|
name-group=
|
||||||
|
no-docstring-rgx=^_
|
||||||
|
property-classes=abc.abstractproperty
|
||||||
|
variable-naming-style=snake_case
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
check-protected-access-in-special-methods=no
|
||||||
|
defining-attr-methods=__init__,
|
||||||
|
__new__,
|
||||||
|
setUp,
|
||||||
|
__post_init__
|
||||||
|
exclude-protected=_asdict,
|
||||||
|
_fields,
|
||||||
|
_replace,
|
||||||
|
_source,
|
||||||
|
_make
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
valid-metaclass-classmethod-first-arg=mcs
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
exclude-too-few-public-methods=
|
||||||
|
ignored-parents=
|
||||||
|
max-args=5
|
||||||
|
max-attributes=7
|
||||||
|
max-bool-expr=5
|
||||||
|
max-branches=12
|
||||||
|
max-locals=15
|
||||||
|
max-parents=7
|
||||||
|
max-public-methods=20
|
||||||
|
max-returns=6
|
||||||
|
max-statements=50
|
||||||
|
min-public-methods=0
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
overgeneral-exceptions=builtins.BaseException,builtins.Exception
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
expected-line-ending-format=
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
indent-after-paren=4
|
||||||
|
indent-string=' '
|
||||||
|
max-line-length=100
|
||||||
|
max-module-lines=1000
|
||||||
|
single-line-class-stmt=no
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
allow-any-import-level=
|
||||||
|
allow-reexport-from-package=no
|
||||||
|
allow-wildcard-with-all=no
|
||||||
|
deprecated-modules=
|
||||||
|
ext-import-graph=
|
||||||
|
import-graph=
|
||||||
|
int-import-graph=
|
||||||
|
known-standard-library=
|
||||||
|
known-third-party=enchant
|
||||||
|
preferred-modules=
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
logging-format-style=old
|
||||||
|
logging-modules=logging
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
confidence=HIGH,
|
||||||
|
CONTROL_FLOW,
|
||||||
|
INFERENCE,
|
||||||
|
INFERENCE_FAILURE,
|
||||||
|
UNDEFINED
|
||||||
|
disable=raw-checker-failed,
|
||||||
|
bad-inline-option,
|
||||||
|
locally-disabled,
|
||||||
|
file-ignored,
|
||||||
|
suppressed-message,
|
||||||
|
useless-suppression,
|
||||||
|
deprecated-pragma,
|
||||||
|
use-symbolic-message-instead,
|
||||||
|
missing-module-docstring,
|
||||||
|
missing-class-docstring,
|
||||||
|
missing-function-docstring,
|
||||||
|
broad-exception-caught
|
||||||
|
enable=c-extension-no-member
|
||||||
|
|
||||||
|
[METHOD_ARGS]
|
||||||
|
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
notes=FIXME,
|
||||||
|
XXX,
|
||||||
|
TODO
|
||||||
|
notes-rgx=
|
||||||
|
|
||||||
|
[REFACTORING]
|
||||||
|
max-nested-blocks=5
|
||||||
|
never-returning-functions=sys.exit,argparse.parse_error
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||||
|
msg-template=
|
||||||
|
reports=no
|
||||||
|
score=yes
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
ignore-comments=yes
|
||||||
|
ignore-docstrings=yes
|
||||||
|
ignore-imports=yes
|
||||||
|
ignore-signatures=yes
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
[SPELLING]
|
||||||
|
max-spelling-suggestions=4
|
||||||
|
spelling-dict=
|
||||||
|
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||||
|
spelling-ignore-words=
|
||||||
|
spelling-private-dict-file=
|
||||||
|
spelling-store-unknown-words=no
|
||||||
|
|
||||||
|
[STRING]
|
||||||
|
check-quote-consistency=no
|
||||||
|
check-str-concat-over-line-jumps=no
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
contextmanager-decorators=contextlib.contextmanager
|
||||||
|
generated-members=
|
||||||
|
ignore-none=yes
|
||||||
|
ignore-on-opaque-inference=yes
|
||||||
|
ignored-checks-for-mixins=no-member,
|
||||||
|
not-async-context-manager,
|
||||||
|
not-context-manager,
|
||||||
|
attribute-defined-outside-init
|
||||||
|
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||||
|
missing-member-hint=yes
|
||||||
|
missing-member-hint-distance=1
|
||||||
|
missing-member-max-choices=1
|
||||||
|
mixin-class-rgx=.*[Mm]ixin
|
||||||
|
signature-mutators=
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
additional-builtins=
|
||||||
|
allow-global-unused-variables=yes
|
||||||
|
allowed-redefined-builtins=
|
||||||
|
callbacks=cb_,
|
||||||
|
_cb
|
||||||
|
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||||
|
ignored-argument-names=_.*|^ignored_|^unused_
|
||||||
|
init-import=no
|
||||||
|
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
requests==2.31.0
|
||||||
|
aiogram==2.25.1
|
||||||
|
pydantic[dotenv]==1.10.8
|
Loading…
Reference in a new issue