Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Archie committed Jan 9, 2021
2 parents dda1d69 + 0b49807 commit 54e7c63
Show file tree
Hide file tree
Showing 15 changed files with 510 additions and 410 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ venv/
__pycache__
*.spec
build
dist
dist
*.zip
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,38 @@ _От автора:_
Я не несу ответственности за ваши номера (если они улетят в бан или еще чего по-хуже).
Скрипт не делает ничего запрещенного, а лишь "нажимает кнопочки, которые вы могли
бы нажать в их приложении, потратив в 10 (да-да) раз больше времени".

Скрипт не ворует данные и не каким образом не взаимодействует ни с чем-либо кроме
оффициального (но не публичного) API Tele2.
Простое консольное прилоджение сделано лишь для того, чтобы у пользователя не
возникало сомнений в честности действий, выполняемых скриптом.
Вы можете посмотреть исходники и подредактировать код, если в том есть неоходимость.
Подобные приложения с веб-интерфейсом и т.п вынуждены хранить ваши авторизационные токены на своих серверах,
что может привести с потере доступа к аккаунту.
Т.к существует возможность бесконечного продления токена без необходимости
повторного ввода СМС-кода, ввели смс в типичное мошенническое веб-приложение
1 раз и злоумышленник получит доступ к вашему аккаунту НАВСЕГДА (да).


Если возникли какие-либо проблемы в работе - откройте обсуждение на вкладке Issues и,
если, нашли решение, предложите автору, буду очень признателен. Так как скрипт очень
чувствителен к региону, из которого он запускается, проблемы возникнуть могут, и не факт,
что конкретно ваша проблема вообще решаема (например, старый тариф без поддержки
Маркета и т.п). Спасибо за понимание)
если, нашли решение, приложите свои комментарии. Так как скрипт очень
чувствителен к региону, из которого он запускается, могут возникнуть проблемы в работе, и не факт,
что конкретно ваша проблема решаема (например, старый тариф без поддержки
Маркета и т.п).

Спасибо за понимание)


## Features
* Quick market listing of your Tele2 data
* Bumping up lots that haven't been sold
* Asynchronous queries to _Tele2 API_ allow to perform multiple actions simultaneously
* __[v1.3] Lots auto re-listing after some user-provided interval__
* Lots auto re-listing after some user-provided interval
* **[v2.0+] Token auto-refreshing without extra SMS inputs**


## Demo (v1.0.0 on Windows 10)
![imgur demo gif](https://i.imgur.com/xKTTRDS.gif)
## Demo (v2.0.0 on Windows 10 Terminal)
![imgur demo gif](https://i.imgur.com/Ciy2tp3.gif)


## Installation (basic - *Windows x64 only [x32 work in progress]*)
Expand Down Expand Up @@ -62,9 +76,9 @@ and download **zip**-archive (tele2-profit@\<version\>.zip)


## Usage
1. Login with running `python auth.py` (or auth.exe if built version). Access token works 4 hours, then it needs to be updated.
**note: access-token saves on your PC _only_, in `./config.json` file**
2. Run `python main.py` (or main.exe if built version) and select action.
Run `python main.py` (or main.exe if built version), login, and select action.

**note: Access-token saves on your PC _only_, in `./config.json` file**

### FYI: Current Tele2 market lot requirements

Expand All @@ -84,12 +98,8 @@ For example: `60 80` - 60 minutes (or gb) will be listed for 80 rub.
For example: `68` - 68 minutes (or gb) will be listed with **minimum** possible price *(in this case - 55 rub if minutes, 1020 rub if gb)*.
When done leave input field empty (just hit enter) and you will jump to the next part.


## TODO
* Use refresh token to support longer auth persistence (currently 4 hours)

## Donations
Special thanks to my donators:
Huge thanks to my donors:
* Кирилл - 100 rub
* Alex - 300 rub
* Никита - 300 rub
Expand Down
31 changes: 31 additions & 0 deletions _build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
import shutil
import zipfile

from _version import __version__


def remove_dir_if_exists(path):
if os.path.isdir(path):
shutil.rmtree(path)


def remove_file_if_exists(path):
if os.path.isfile(path):
os.remove(path)


def build_zip():
os.system('pyinstaller --onefile --paths venv main.py')
with zipfile.ZipFile(f'tele2-profit@{__version__}.zip', 'w',
zipfile.ZIP_DEFLATED) as zip_file:
zip_file.write('dist/main.exe', 'main.exe')


if __name__ == '__main__':
remove_file_if_exists(f'tele2-profit@{__version__}.zip')
build_zip()
remove_dir_if_exists('dist')
remove_dir_if_exists('build')
remove_dir_if_exists('__pycache__')
remove_file_if_exists('main.spec')
1 change: 1 addition & 0 deletions _version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '2.0.0'
21 changes: 21 additions & 0 deletions app/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from colorama import Fore

from app.api import Tele2Api


async def print_balance(api):
balance = await api.get_balance()
print(Fore.YELLOW + 'Balance: ' + Fore.MAGENTA + f'{balance} rub.')


async def print_rests(api: Tele2Api):
print('Checking your rests...')
print(
Fore.CYAN + 'note: only plan (not market-bought ones nor transferred)'
' rests can be sold')
rests = await api.get_rests()
print(Fore.WHITE + 'You have')
print(Fore.YELLOW + f'\t{rests["voice"]} min')
print(Fore.GREEN + f'\t{rests["data"]} gb')
print(Fore.WHITE + '\t\tavailable to sell.')
return rests
81 changes: 53 additions & 28 deletions api.py → app/api.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,81 @@
from aiohttp import ClientSession
from time import sleep
from aiohttp import ClientSession, ContentTypeError, ClientResponse

from app.constants import SECURITY_BYPASS_HEADERS, MAIN_API, SMS_VALIDATION_API, \
TOKEN_API


async def _try_parse_to_json(response: ClientResponse):
try:
response_json = await response.json()
return response_json
except ContentTypeError:
return None


def _is_ok(response: ClientResponse):
return response.status == 200


class Tele2Api:
session: ClientSession
access_token: str

def __init__(self, phone_number: str, access_token: str = ''):
base_api = f'https://my.tele2.ru/api/subscribers/{phone_number}'
def __init__(self, phone_number: str, access_token: str = '',
refresh_token: str = ''):
base_api = MAIN_API + phone_number
self.market_api = f'{base_api}/exchange/lots/created'
self.rests_api = f'{base_api}/rests'
self.profile_api = f'{base_api}/profile'
self.balance_api = f'{base_api}/balance'
self.sms_post_url = f'https://my.tele2.ru/api/validation/number/{phone_number}'
self.auth_post_url = 'https://my.tele2.ru/auth/realms/tele2-b2c/protocol/openid-connect/token'
self.sms_post_url = SMS_VALIDATION_API + phone_number
self.auth_post_url = TOKEN_API
self.access_token = access_token
self.refresh_token = refresh_token

async def __aenter__(self):
self.session = ClientSession(headers={
'Authorization': f'Bearer {self.access_token}',
'Connection': 'keep-alive',
'Tele2-User-Agent': '"mytele2-app/3.17.0"; "unknown"; "Android/9"; "Build/12998710"',
'X-API-Version': '1',
'User-Agent': 'okhttp/4.2.0'
**SECURITY_BYPASS_HEADERS
})
return self

async def __aexit__(self, *args):
await self.session.close()

async def check_if_authorized(self):
response = await self.session.get(self.profile_api)
return _is_ok(response)

async def send_sms_code(self):
await self.session.post(self.sms_post_url, json={'sender': 'Tele2'})

async def get_access_token(self, phone_number: str, sms_code: str):
async def auth_with_code(self, phone_number: str, sms_code: str):
response = await self.session.post(self.auth_post_url, data={
'client_id': 'digital-suite-web-app',
'grant_type': 'password',
'username': phone_number,
'password': sms_code,
'password_type': 'sms_code'
})
return (await response.json())['access_token']
if _is_ok(response):
response_json = await _try_parse_to_json(response)
return response_json['access_token'], response_json['refresh_token']

async def check_auth_code(self):
response = await self.session.get(self.profile_api)
return response.status
async def refresh_tokens(self, refresh_token: str):
response = await self.session.post(self.auth_post_url, data={
'client_id': 'digital-suite-web-app',
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
})
if _is_ok(response):
response_json = await _try_parse_to_json(response)
return response_json['access_token'], response_json['refresh_token']

async def get_balance(self):
response = await self.session.get(self.balance_api)
return (await response.json())['data']['value']
if _is_ok(response):
response_json = await _try_parse_to_json(response)
return response_json['data']['value']

async def sell_lot(self, lot):
response = await self.session.put(self.market_api, json={
Expand All @@ -57,27 +84,25 @@ async def sell_lot(self, lot):
'volume': {'value': lot['amount'],
'uom': 'min' if lot['lot_type'] == 'voice' else 'gb'}
})
# TODO: вместо ожидания 0.5 поставить что-то более полезное,
# одновременно не получается выставлять больше (((
sleep(0.5)
return await response.json()

return await _try_parse_to_json(response)

async def return_lot(self, lot_id):
response = await self.session.delete(f'{self.market_api}/{lot_id}')
# TODO: вместо ожидания 0.5 поставить что-то более полезное,
# одновременно не получается выставлять больше (((
sleep(0.5)
return await response.json()
return await _try_parse_to_json(response)

async def get_active_lots(self):
response = await self.session.get(self.market_api)
lots = list((await response.json())['data'])
active_lots = [a for a in lots if a['status'] == 'active']
return active_lots
if _is_ok(response):
response_json = await _try_parse_to_json(response)
lots = list(response_json['data'])
active_lots = [a for a in lots if a['status'] == 'active']
return active_lots

async def get_rests(self):
response = await self.session.get(self.rests_api)
rests = list((await response.json())['data']['rests'])
response_json = await _try_parse_to_json(response)
rests = list(response_json['data']['rests'])
sellable = [a for a in rests if a['type'] == 'tariff']
return {
'data': int(
Expand Down
39 changes: 39 additions & 0 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json
import re
from colorama import Fore

from app.api import Tele2Api


def input_phone_number():
while True:
user_input = input(
Fore.CYAN +
f'Input your Tele2 phone number (leave empty to cancel): '
)
if re.match(r'^7\d{10}?$', user_input):
return user_input
elif user_input == '':
exit()
else:
print(Fore.RED + 'Incorrect number format. Correct - 7xxxxxxxxxx')


async def get_tokens(api: Tele2Api, phone_number: str):
await api.send_sms_code()
while True:
try:
sms_code = input(Fore.LIGHTCYAN_EX + 'SMS code: ')
return await api.auth_with_code(phone_number, sms_code)
except KeyError:
print(Fore.RED + 'Invalid SMS-сode. Try again')


def write_config_to_file(phone_number: str, access_token: str,
refresh_token: str):
with open('config.json', 'w') as f:
json.dump({
'x-p': phone_number,
'x-at': access_token,
'x-rt': refresh_token,
}, f, indent=2)
10 changes: 10 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SECURITY_BYPASS_HEADERS = {
'Connection': 'keep-alive',
'Tele2-User-Agent': '"mytele2-app/3.17.0"; "unknown"; "Android/9"; "Build/12998710"',
'X-API-Version': '1',
'User-Agent': 'okhttp/4.2.0'
}

MAIN_API = 'https://my.tele2.ru/api/subscribers/'
SMS_VALIDATION_API = 'https://my.tele2.ru/api/validation/number/'
TOKEN_API = 'https://my.tele2.ru/auth/realms/tele2-b2c/protocol/openid-connect/token/'

0 comments on commit 54e7c63

Please sign in to comment.