├── .gitignore ├── README.md ├── api ├── __init__.py ├── service_apis │ ├── __init__.py │ ├── blockchain_rates.py │ ├── cbr_valutes.py │ ├── covid19.py │ ├── interfaces.py │ ├── qiwi.py │ ├── rss.py │ ├── weatherstack.py │ └── wttr_in.py └── social_nets │ ├── __init__.py │ ├── interfaces.py │ ├── telegram.py │ └── vkontakte.py ├── config.py ├── requirements.txt ├── sad.jpg ├── screenshoot.png ├── sender.py └── service └── registration.py /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | .idea 3 | __pycache__ 4 | /.env 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Проснулся утром и сразу полез залипать в смартфон, чтобы узнать несколько базовых (для меня) вещей: какая сегодня погода, че по курсам валют, какие новости, сколько входящих в почте. На всё это можно потратить достаточно времени, поэтому быстро было решено написать скрипт, который каждое утро отсылает всё это в одном сообщении. 2 | 3 | # Представляю вам Morgenpost! 4 | 5 | ## Что умеет: 6 | * Курс обмена валют c Qiwi 7 | * Погода с Weatherstack 8 | * Новости с RSS 9 | * Отсылка в Telegram или VK 10 | * А ещё много чего, вы только пишите ишью и оставляйте пулреквесты, хоть иногда... 11 | ![:(](sad.jpg) 12 | 13 | ## Как заполнять конфиг 14 | Да он и так отлично отдкументирован! Но если что...: 15 | * Если где-то нужно ввести API KEY, а там пустая строка, то этот модуль использоваться не будет 16 | * Токены (API KEY) заполнять в отдельном файле .env. 17 | 18 | ## Как запускать 19 | * Качаешь python 3.8 (я на нём тестил) 20 | * Ставишь библиотечки через `pip3 install -r requirements.txt` 21 | * Идешь в крон и запускаешь этот скрипт в время, когда ты можешь проснуться 22 | * Profit! 23 | 24 | ## Как настроить отправку: каких сервисов, в какую соцсеть 25 | Всё достаточно просто. 26 | * По умолчанию в sender.py подключены все сервисы, ненужные строчки можно просто удалить или закомментировать. 27 | * Для Телеграма и ВКонтакте подключение одинаковое. Если хотите ВК, то в строке, которая показана ниже, просто замените SocialNetType.Telegram на SocialNetType.VK 28 | > social_net = SocialNet(SocialNetType.Telegram, TELEGRAM_API_TOKEN) 29 | 30 | ## Как это выглядит 31 | ![Я бог](screenshoot.png) -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiriharu/morgenpost/57690a70988721fb3f8c1f97e8ef5ba6ee5d95a9/api/__init__.py -------------------------------------------------------------------------------- /api/service_apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiriharu/morgenpost/57690a70988721fb3f8c1f97e8ef5ba6ee5d95a9/api/service_apis/__init__.py -------------------------------------------------------------------------------- /api/service_apis/blockchain_rates.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from decimal import Decimal, ROUND_HALF_UP 4 | from typing import List 5 | import requests 6 | 7 | from api.service_apis.interfaces import IApi 8 | 9 | 10 | class BlockchainConfig: 11 | def __init__(self, blockchain_rates: List[str]): 12 | self.blockchain_rates = blockchain_rates 13 | 14 | 15 | @dataclass 16 | class BlockchainRatesInfo: 17 | symbol: str 18 | price: Decimal 19 | 20 | def __str__(self) -> str: 21 | return f"💰 За 1{self.symbol} дают {self.price}USD\n" 22 | 23 | 24 | class BlockchainRates(IApi): 25 | def __init__(self, config: BlockchainConfig): 26 | self.symbols = config.blockchain_rates 27 | 28 | @property 29 | def url(self): 30 | return "http://api.coincap.io/v2/assets" 31 | 32 | @property 33 | def header(self): 34 | return "🏦Курс криптовалют: \n\n" 35 | 36 | def get(self) -> str: 37 | result = (requests.get(self.url).json())["data"] 38 | message = self.header 39 | for currency in result: 40 | if currency["symbol"] in self.symbols: 41 | price = Decimal(currency["priceUsd"]) 42 | price = price.quantize(Decimal("1.0000"), ROUND_HALF_UP) 43 | message += str(BlockchainRatesInfo( 44 | symbol=currency["symbol"], 45 | price=price 46 | )) 47 | message += "\n" 48 | return message 49 | -------------------------------------------------------------------------------- /api/service_apis/cbr_valutes.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import List 3 | import requests 4 | from dataclasses import dataclass 5 | from decimal import Decimal 6 | 7 | from api.service_apis.interfaces import IApi 8 | 9 | 10 | class CBRConfig: 11 | def __init__(self, cross_rates: List[str]): 12 | self.cross_rates = cross_rates 13 | 14 | 15 | @dataclass 16 | class CbrValutesInfo: 17 | valute: str 18 | nominal: int 19 | value: Decimal 20 | 21 | def __str__(self): 22 | return f"💰 За {self.nominal}{self.valute} дают {self.value}RUB\n" 23 | 24 | 25 | class CbrValutes(IApi): 26 | def __init__(self, config: CBRConfig): 27 | self.valutes: List[str] = config.cross_rates 28 | 29 | @property 30 | def url(self): 31 | return "https://www.cbr-xml-daily.ru/daily_json.js" 32 | 33 | @property 34 | def header(self): 35 | return "🏦Курс валют ЦБР: \n\n" 36 | 37 | def get(self) -> str: 38 | result = (requests.get(self.url).json())["Valute"] 39 | message = self.header 40 | 41 | for valute in self.valutes: 42 | if valute in result: 43 | 44 | message += str(CbrValutesInfo( 45 | valute=valute, 46 | nominal=result[valute]["Nominal"], 47 | value=Decimal(str(result[valute]["Value"])) 48 | )) 49 | 50 | message += "\n" 51 | return message 52 | -------------------------------------------------------------------------------- /api/service_apis/covid19.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import List 3 | import requests 4 | from dataclasses import dataclass 5 | 6 | from api.service_apis.interfaces import IApi 7 | 8 | 9 | class Covid19Config: 10 | def __init__(self, countries: List[str], mode: str): 11 | self.countries = countries 12 | self.mode = mode 13 | 14 | 15 | @dataclass 16 | class Covid19Info: 17 | mode: str 18 | country: str 19 | cases: int 20 | today_cases: int 21 | deaths: int 22 | active: int 23 | today_deaths: int = None 24 | recovered: int = None 25 | critical: int = None 26 | 27 | def __str__(self) -> str: 28 | if self.mode == "EXTENDED": 29 | return f"🦠Страна: {self.country}\n" \ 30 | f"Всего: {self.cases}\n" \ 31 | f"➕Болеют всего: {self.active}; сегодня: {self.today_cases}\n" \ 32 | f"⚰️Умерших всего: {self.deaths}; сегодня: {self.today_deaths}\n" \ 33 | f"🤒В крит. состоянии: {self.critical}\n" \ 34 | f"😷Выздоровевших: {self.recovered}\n\n" 35 | elif self.mode == "SHORT": 36 | return f"🦠{self.country}:\n" \ 37 | f"⚰️{self.deaths}, ➕{self.today_cases}, 🤒{self.active}\n" \ 38 | f"😷{self.cases}\n\n" 39 | 40 | 41 | class Covid19(IApi): 42 | 43 | def __init__(self, config: Covid19Config): 44 | self.countries = config.countries 45 | 46 | if (config.mode == "") or (config.mode != "SHORT" and config.mode != "EXTENDED"): 47 | self.mode = "EXTENDED" 48 | else: 49 | self.mode = config.mode 50 | 51 | @property 52 | def url(self): 53 | return "https://coronavirus-19-api.herokuapp.com/countries/" 54 | 55 | @property 56 | def header(self): 57 | return "🦠Статистика по коронавирусу: \n\n" 58 | 59 | def get(self) -> str: 60 | message = "" 61 | for country in self.countries: 62 | url = self.url + country 63 | result = requests.get(url) 64 | if result.content != b'Country not found': 65 | result = result.json() 66 | if self.mode == "EXTENDED": 67 | message += str(Covid19Info( 68 | mode=self.mode, 69 | country=country, 70 | cases=result["cases"], 71 | today_cases=result["todayCases"], 72 | active=result["active"], 73 | deaths=result["deaths"], 74 | today_deaths=result["todayDeaths"], 75 | recovered=result["recovered"], 76 | critical=result["critical"] 77 | )) 78 | elif self.mode == "SHORT": 79 | message += str(Covid19Info( 80 | mode=self.mode, 81 | country=country, 82 | cases=result["cases"], 83 | today_cases=result["todayCases"], 84 | active=result["active"], 85 | deaths=result["deaths"], 86 | )) 87 | 88 | if message == "": 89 | message = "Статистика отсутствует, проверьте данные в массиве!" 90 | 91 | message += "\n" 92 | return message 93 | -------------------------------------------------------------------------------- /api/service_apis/interfaces.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod, abstractproperty 2 | 3 | 4 | class IApi: 5 | __metaclass__ = ABCMeta 6 | 7 | @abstractmethod 8 | def get(self, *args, **kwargs) -> str: 9 | """Get info from API""" 10 | 11 | @property 12 | @abstractmethod 13 | def url(self): 14 | """API's url""" 15 | raise NotImplementedError 16 | 17 | @property 18 | @abstractmethod 19 | def header(self): 20 | """API's header""" 21 | raise NotImplementedError 22 | -------------------------------------------------------------------------------- /api/service_apis/qiwi.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import List, Tuple 3 | 4 | import requests 5 | from dataclasses import dataclass 6 | 7 | from api.service_apis.interfaces import IApi 8 | 9 | 10 | def replace_by_name(code): 11 | if code == "643": 12 | return "RUB" 13 | if code == "978": 14 | return "EUR" 15 | if code == "840": 16 | return "USD" 17 | 18 | 19 | class QiwiConfig: 20 | def __init__(self, token: str, cross_rates: List[Tuple[str, str]]): 21 | self.token = token 22 | self.cross_rates = cross_rates 23 | 24 | 25 | @dataclass 26 | class CrossRate: 27 | from_e: str 28 | to: str 29 | rate: float 30 | 31 | def __str__(self) -> str: 32 | return f"💰 За 1 {replace_by_name(self.to)} дают {replace_by_name(self.from_e)} {self.rate}\n" 33 | 34 | 35 | class Qiwi(IApi): 36 | 37 | def __init__(self, config: QiwiConfig): 38 | self.valutes = config.cross_rates 39 | self.session = requests.Session() 40 | self.session.headers = { 41 | "authorization": f"Bearer {config.token}", 42 | "content-type": "application/json", 43 | "Accept": "application/json" 44 | } 45 | 46 | @property 47 | def url(self): 48 | return "https://edge.qiwi.com" 49 | 50 | @property 51 | def header(self): 52 | return "🥝Курс в обменнике Qiwi: \n\n" 53 | 54 | def call(self, method: str) -> dict: 55 | result = self.session.get(f"{self.url}{method}") 56 | if result.status_code == 401: 57 | raise Exception("Authorization failed") 58 | return result.json()['result'] 59 | 60 | def cross_rates(self): 61 | return self.call("/sinap/crossRates") 62 | 63 | def get(self) -> str: 64 | message = self.header 65 | for rates in self.valutes: 66 | rate_from, rate_to = rates[0], rates[1] 67 | crossrate_dict = [x for x in self.cross_rates() 68 | if x['from'] == rate_from and x['to'] == rate_to][0] 69 | 70 | message += str(CrossRate( 71 | from_e=crossrate_dict['from'], 72 | to=crossrate_dict['to'], 73 | rate=crossrate_dict['rate'] 74 | )) 75 | message += "\n" 76 | return message 77 | -------------------------------------------------------------------------------- /api/service_apis/rss.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | import feedparser 4 | import requests 5 | from dataclasses import dataclass 6 | 7 | 8 | class RSSConfig: 9 | def __init__(self, max_entries: int, feeds: List[Tuple[str, str]]): 10 | self.max_entries = max_entries 11 | self.feeds = feeds 12 | 13 | 14 | @dataclass 15 | class NewsObj: 16 | link: str 17 | title: str 18 | 19 | def __str__(self): 20 | return f"[{self.title}]({self.link})" 21 | 22 | 23 | class Feed: 24 | 25 | def __init__(self, name, links, max_entries): 26 | self.name = name 27 | self.links = links 28 | self.max_entries = max_entries 29 | 30 | def get(self): 31 | strings = [] 32 | parsed = feedparser.parse(self.links) 33 | for field in parsed['entries'][0:self.max_entries]: 34 | strings.append(NewsObj( 35 | link=field.get('link'), 36 | title=field.get('title', 'invalid_title') 37 | )) 38 | 39 | feed_message = f"🗞 {self.name}\n" 40 | for entry in strings: 41 | feed_message += f"📍 {str(entry)}\n" 42 | 43 | feed_message += "\n" 44 | return feed_message 45 | 46 | 47 | class RSS: 48 | 49 | def __init__(self, config: RSSConfig): 50 | self.feeds = config.feeds 51 | self.max_entries = config.max_entries 52 | 53 | def get(self): 54 | rss_message = "" 55 | if self.max_entries > 0 and len(self.feeds) > 0: 56 | for feed in self.feeds: 57 | rss_message += Feed(feed[0], feed[1], self.max_entries).get() 58 | 59 | rss_message += "\n" 60 | return rss_message 61 | -------------------------------------------------------------------------------- /api/service_apis/weatherstack.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import List 3 | 4 | import requests 5 | from dataclasses import dataclass 6 | 7 | from api.service_apis.interfaces import IApi 8 | 9 | 10 | class WeatherStackConfig: 11 | def __init__(self, token: str, locations: List[str]): 12 | self.token = token 13 | self.locations = locations 14 | 15 | 16 | @dataclass 17 | class WeatherBasicInfo: 18 | temperature: int 19 | feelslike: int 20 | humidity: int 21 | pressure: int 22 | wind_speed: int 23 | name: str 24 | weather_descriptions: str 25 | 26 | def __str__(self) -> str: 27 | return f"{self.name} : {self.weather_descriptions}\n" \ 28 | f"🌡{self.temperature}°C, ощущается как {self.feelslike}°C\n" \ 29 | f"💨{self.wind_speed}, 💧{self.humidity}%, ⬇️ {self.pressure}\n" 30 | 31 | 32 | class WeatherStack(IApi): 33 | 34 | def __init__(self, config: WeatherStackConfig): 35 | self.locations = config.locations 36 | self.access_key = config.token 37 | 38 | @property 39 | def url(self): 40 | return "http://api.weatherstack.com/current" 41 | 42 | @property 43 | def header(self): 44 | return "☀️Погода сейчас: \n\n" 45 | 46 | def call(self, params: dict) -> dict: 47 | params["access_key"] = self.access_key 48 | result = requests.get(self.url, params).json() 49 | # оно тут возвращает success: False, если ошибки. 50 | if not result.get("success", True): 51 | if result["error"]["code"] == 604: 52 | raise Exception("Bulk not supported") 53 | if result["error"]["code"] == 101: 54 | raise Exception("Invalid access key") 55 | if result["error"]["code"] == 105: 56 | raise Exception("Access Restricted") 57 | raise Exception(result["error"]) 58 | return result 59 | 60 | def get(self) -> str: 61 | message = self.header 62 | 63 | for location in self.locations: 64 | query = dict(query=location) 65 | response = self.call(query) 66 | message += str(WeatherBasicInfo( 67 | temperature=response['current']['temperature'], 68 | feelslike=response['current']['feelslike'], 69 | humidity=response['current']['humidity'], 70 | pressure=response['current']['pressure'], 71 | wind_speed=response['current']['wind_speed'], 72 | name=response['request']['query'], 73 | weather_descriptions="".join(response['current']['weather_descriptions']) 74 | )) 75 | 76 | message += "\n" 77 | return message 78 | -------------------------------------------------------------------------------- /api/service_apis/wttr_in.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from typing import List 4 | 5 | import requests 6 | 7 | from api.service_apis.interfaces import IApi 8 | 9 | 10 | class WttrInConfig: 11 | def __init__(self, locations: List[str]): 12 | self.locations = locations 13 | 14 | 15 | @dataclass 16 | class WttrInInfo: 17 | city: str 18 | temperature: str 19 | feels_like_C: str 20 | cloudcover: str 21 | humidity: str 22 | weather: str 23 | uv_index: str 24 | visibility: str 25 | wind_speed: str 26 | 27 | def __str__(self) -> str: 28 | return f"🏙 {self.city}, " \ 29 | f"{self.weather.lower()}\n" \ 30 | f"🌡{self.temperature}°C, ощущается как {self.feels_like_C}°C, " \ 31 | f"💧{self.humidity}%, " \ 32 | f"🔮{self.uv_index}\n" \ 33 | f"💨{self.wind_speed}km/h, " \ 34 | f"👁{self.visibility}/10, " \ 35 | f"☁{self.cloudcover}\n" 36 | 37 | 38 | class WttrIn(IApi): 39 | def __init__(self, config: WttrInConfig): 40 | self.cities = config.locations 41 | 42 | @property 43 | def url(self): 44 | return "https://wttr.in/" 45 | 46 | @property 47 | def header(self): 48 | return "☀️Погода сейчас: \n\n" 49 | 50 | def get(self) -> str: 51 | message = self.header 52 | for city in self.cities: 53 | url = self.url + f"{city}?0&format=j1&lang=ru&m&M" 54 | result = ((requests.get(url).json())['current_condition'])[0] 55 | message += str(WttrInInfo( 56 | city=city, 57 | temperature=result['temp_C'], 58 | feels_like_C=result['FeelsLikeC'], 59 | cloudcover=result['cloudcover'], 60 | humidity=result['humidity'], 61 | weather=result['lang_ru'][0]['value'], 62 | uv_index=result['uvIndex'], 63 | visibility=result['visibility'], 64 | wind_speed=result['windspeedKmph'] 65 | )) 66 | message += "\n" 67 | return message 68 | -------------------------------------------------------------------------------- /api/social_nets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiriharu/morgenpost/57690a70988721fb3f8c1f97e8ef5ba6ee5d95a9/api/social_nets/__init__.py -------------------------------------------------------------------------------- /api/social_nets/interfaces.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod, abstractproperty 2 | 3 | from typing import Union 4 | 5 | 6 | class ISocialNet: 7 | __metaclass__ = ABCMeta 8 | 9 | @abstractmethod 10 | def call(self, method: str, params: dict) -> dict: 11 | """Call API method""" 12 | raise NotImplementedError 13 | 14 | @abstractmethod 15 | def send_message(self, chat_id: Union[str, int], text: str) -> dict: 16 | """Send message to chat""" 17 | raise NotImplementedError 18 | 19 | @abstractmethod 20 | def send(self, text: str, chat_id: Union[str, int]) -> None: 21 | """Safety send message to chat""" 22 | raise NotImplementedError 23 | 24 | @property 25 | @abstractmethod 26 | def api_url(self): 27 | """API's url""" 28 | raise NotImplementedError 29 | -------------------------------------------------------------------------------- /api/social_nets/telegram.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from typing import Union, List 3 | from api.social_nets.interfaces import ISocialNet 4 | from abc import ABC 5 | 6 | 7 | class Telegram(ISocialNet): 8 | 9 | def __init__(self, token: str): 10 | self.token = token 11 | 12 | @property 13 | def api_url(self): 14 | return "https://api.telegram.org" 15 | 16 | def call(self, method: str, params: dict) -> dict: 17 | url = f"{self.api_url}/bot{self.token}/{method}" 18 | return requests.get(url, params).json() 19 | 20 | def send_message(self, chat_id: Union[str, int], text: str) -> dict: 21 | return self.call("sendMessage", params=dict( 22 | chat_id=chat_id, text=text, parse_mode="Markdown", disable_web_page_preview=True 23 | )) 24 | 25 | def send(self, text: str, chat_id: Union[str, int]) -> None: 26 | if len(text) > 4096: 27 | for x in range(0, len(text), 4096): 28 | self.send_message(chat_id, text[x:x+4096]) 29 | else: 30 | self.send_message(chat_id, text) 31 | -------------------------------------------------------------------------------- /api/social_nets/vkontakte.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from typing import Union, List 3 | from random import randint 4 | from abc import ABC 5 | from api.social_nets.interfaces import ISocialNet 6 | 7 | 8 | class Vkontakte(ISocialNet): 9 | 10 | def __init__(self, token: str): 11 | self.token = token 12 | 13 | @property 14 | def api_url(self): 15 | return "https://api.vk.com/method/" 16 | 17 | def call(self, method: str, params: dict) -> dict: 18 | url = f"{self.api_url}{method}" 19 | return requests.get(url, params).json() 20 | 21 | def send_message(self, user_id: Union[str, int], text: str) -> dict: 22 | return self.call("messages.send", params=dict( 23 | user_id=user_id, 24 | message=text, 25 | random_id=randint(int(-2e9), 26 | int(2e9)), 27 | access_token=self.token, 28 | v=999.9 29 | )) 30 | 31 | def send(self, text: str, chat_id: Union[str, int]) -> None: 32 | self.send_message(chat_id, text) 33 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import typing 4 | 5 | from dotenv import load_dotenv 6 | 7 | from api.service_apis.blockchain_rates import BlockchainConfig 8 | from api.service_apis.cbr_valutes import CBRConfig 9 | from api.service_apis.covid19 import Covid19Config 10 | from api.service_apis.qiwi import QiwiConfig 11 | from api.service_apis.rss import RSSConfig 12 | from api.service_apis.weatherstack import WeatherStackConfig 13 | from api.service_apis.wttr_in import WttrInConfig 14 | 15 | dotenv_path = os.path.join(os.path.dirname(__file__), '.env') 16 | if os.path.exists(dotenv_path): 17 | load_dotenv(dotenv_path) 18 | 19 | # Бот токен от телеги вида 71231246:WUGvG2D412415ssFasf3YT6HTTs1 20 | TELEGRAM_API_TOKEN = os.getenv("TELEGRAM_API_TOKEN") 21 | # ID или юзернеймы (но лучше ID) пользователей телеги, которым посылается сводка 22 | TELEGRAM_USERS_ID = json.loads(os.getenv("TELEGRAM_USERS_ID")) 23 | 24 | # Стартовое сообщение 25 | STARTING_MESSAGE = "Доброе утро! Вот тебе сводка данных с утра: \n\n" 26 | 27 | # Киви токен, получаем тут: https://qiwi.com/api 28 | QIWI_TOKEN = os.getenv("QIWI_TOKEN") 29 | # Инфа об обмене валют, вида (откуда, в какую) 30 | QIWI_CROSS_RATES = [("643", "978"), ("643", "840")] 31 | 32 | # Информация об обмене валют в массиве. 33 | # Будет дано сколько рублей стоит определенное количество единиц валюты, выданное РБК. 34 | CBR_CROSS_RATES = ["USD", "EUR", "ZAR"] 35 | 36 | # Курс каких криптовалют получать 37 | # Обозначения валют на английском 38 | BLOCKCHAIN_RATES = ["BTC", "USDT", "XEM"] 39 | 40 | # https://weatherstack.com 1000 вызовов в месяц, шикарно, нам хватит. 41 | WEATHERSTACK_API_KEY = os.getenv("WEATHERSTACK_API_KEY") 42 | # имена локаций через запятую, на англицком 43 | WEATHERSTACK_LOCATIONS = ["Moscow", "Minsk"] 44 | 45 | # имена локаций, любой Unicode язык 46 | # если пусто: модуль выключается 47 | WTTRIN_LOCATIONS = ["Москва", "Берлин"] 48 | 49 | # COVID19 информация по странам 50 | # Названия стран брать из https://coronavirus-19-api.herokuapp.com/ -> Countries 51 | COVID_COUNTRIES = ["Russia", "India", "Brazil"] 52 | # Режим работы: расширенный "EXTENDED" или краткий "SHORT" 53 | # По умолчанию: EXTENDED 54 | COVID_MODE = "SHORT" 55 | 56 | # Количество записей на каждую RSS ленту 57 | RSS_MAX_ENTRIES = 5 58 | # RSS ленты вида (название, линк) 59 | RSS_FEEDS = [ 60 | ("Новости с Яндекса", "https://news.yandex.ru/world.rss"), 61 | ("Новости с Opennet", "https://www.opennet.ru/opennews/opennews_all_utf.rss"), 62 | ] 63 | 64 | weatherstack_config = WeatherStackConfig(token=WEATHERSTACK_API_KEY, locations=WEATHERSTACK_LOCATIONS) 65 | wttrin_config = WttrInConfig(locations=WTTRIN_LOCATIONS) 66 | qiwi_config = QiwiConfig(token=QIWI_TOKEN, cross_rates=QIWI_CROSS_RATES) 67 | cbr_config = CBRConfig(CBR_CROSS_RATES) 68 | blockchain_config = BlockchainConfig(BLOCKCHAIN_RATES) 69 | covid19_config = Covid19Config(countries=COVID_COUNTRIES, mode=COVID_MODE) 70 | rss_config = RSSConfig(max_entries=RSS_MAX_ENTRIES, feeds=RSS_FEEDS) 71 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2020.6.20 2 | chardet==3.0.4 3 | feedparser==6.0.1 4 | idna==2.10 5 | requests==2.24.0 6 | sgmllib3k==1.0.0 7 | urllib3==1.25.10 8 | python-dotenv~=0.14.0 -------------------------------------------------------------------------------- /sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiriharu/morgenpost/57690a70988721fb3f8c1f97e8ef5ba6ee5d95a9/sad.jpg -------------------------------------------------------------------------------- /screenshoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiriharu/morgenpost/57690a70988721fb3f8c1f97e8ef5ba6ee5d95a9/screenshoot.png -------------------------------------------------------------------------------- /sender.py: -------------------------------------------------------------------------------- 1 | from config import ( 2 | STARTING_MESSAGE, 3 | weatherstack_config, wttrin_config, 4 | qiwi_config, cbr_config, 5 | blockchain_config, covid19_config, 6 | rss_config, 7 | TELEGRAM_API_TOKEN, TELEGRAM_USERS_ID 8 | ) 9 | from api.service_apis import weatherstack, qiwi, blockchain_rates, wttr_in, rss, covid19, cbr_valutes 10 | from service.registration import ApisList, SocialNet, SocialNetType 11 | 12 | if __name__ == "__main__": 13 | 14 | apis = ApisList() 15 | apis.add_api(weatherstack.WeatherStack(weatherstack_config)) 16 | apis.add_api(wttr_in.WttrIn(wttrin_config)) 17 | apis.add_api(qiwi.Qiwi(qiwi_config)) 18 | apis.add_api(cbr_valutes.CbrValutes(cbr_config)) 19 | apis.add_api(blockchain_rates.BlockchainRates(blockchain_config)) 20 | apis.add_api(covid19.Covid19(covid19_config)) 21 | apis.add_api(rss.RSS(rss_config)) 22 | 23 | message = f"{STARTING_MESSAGE}{apis.get_str()}" 24 | 25 | social_net = SocialNet(SocialNetType.Telegram, TELEGRAM_API_TOKEN) 26 | social_net.send(message, TELEGRAM_USERS_ID) 27 | -------------------------------------------------------------------------------- /service/registration.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from enum import Enum 3 | 4 | from api.social_nets.telegram import Telegram 5 | from api.social_nets.vkontakte import Vkontakte 6 | 7 | T = typing.TypeVar("T") 8 | 9 | 10 | class SocialNetType(Enum): 11 | Telegram = 0 12 | VK = 1 13 | Discord = 2 14 | 15 | 16 | class ApisList: 17 | def __init__(self, apis: typing.Optional[typing.List[T]] = None): 18 | if apis is None: 19 | self.work_api = [] 20 | else: 21 | self.work_api: typing.List[T] = apis 22 | 23 | def init_apis(self): 24 | for api in self.work_api: 25 | self.work_api += api.setup_and_get() 26 | 27 | return self 28 | 29 | def add_api(self, api: T): 30 | self.work_api.append(api) 31 | return self 32 | 33 | def get_str(self): 34 | return self.__str__() 35 | 36 | def __str__(self): 37 | message = "" 38 | if self.work_api: 39 | for api in self.work_api: 40 | message += api.get() 41 | message += "\n" 42 | else: 43 | message += "Новостей на сегодня нет, ибо список сервисов пуст!" 44 | 45 | return message 46 | 47 | 48 | class SocialNet: 49 | def __init__(self, type_net: SocialNetType, token: typing.Optional[str] = None): 50 | self.type_net: SocialNet.NetType = type_net 51 | if token is not None: 52 | self.init_net(token) 53 | else: 54 | self.net: T = None 55 | 56 | def send(self, message, chat_ids): 57 | for chat_id in chat_ids: 58 | self.net.send(message, chat_id) 59 | 60 | def init_net(self, token: str): 61 | if self.type_net == SocialNetType.Telegram: 62 | self.net = Telegram(token) 63 | 64 | elif self.type_net == SocialNetType.VK: 65 | self.net = Vkontakte(token) 66 | 67 | elif self.type_net == SocialNetType.Discord: 68 | raise NotImplemented("This social network is currently not supported") 69 | 70 | return self 71 | --------------------------------------------------------------------------------