Add files via upload :trollface:
This commit is contained in:
commit
a9cfc9b522
9 changed files with 425 additions and 0 deletions
16
.env.example
Normal file
16
.env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Pacer credentials
|
||||
EMAIL=''
|
||||
PASSWORD=''
|
||||
|
||||
# Telegram configuration
|
||||
# TELEGRAM_SKIP=true # skip Telegram update
|
||||
TELEGRAM_API_ID=
|
||||
TELEGRAM_API_HASH=
|
||||
|
||||
# Run configuration (you should really change these values lol)
|
||||
CALORIES=383.0
|
||||
DISTANCE=5239.0
|
||||
DURATION=2039
|
||||
STEPS=6239
|
||||
|
||||
# vim: ft=sh
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.env*
|
||||
!.env.example
|
||||
*.session
|
||||
.venv
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM python:3.13-alpine
|
||||
|
||||
# crond refuses to stop on SIGINT for some reason
|
||||
STOPSIGNAL TERM
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY crontab /etc/crontabs/root
|
||||
|
||||
COPY main.py /app/main.py
|
||||
COPY user.session /app/user.session
|
||||
|
||||
CMD ["crond", "-f", "-l2"]
|
||||
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# pwnbars running
|
||||
ha ha it's like kronbars but pwn that's funny right
|
||||
|
||||
now you can run as far as you wish
|
||||
|
||||
kudos to [@mrsobakin](https://github.com/mrsobakin) for some assistance in reverse engineering :trollface:
|
||||
|
||||
|
||||
### Obtain your Telegram API credentials
|
||||
You might require **api_id** and **api_hash** to access the Telegram API servers. To learn how to obtain them [click here](https://core.telegram.org/api/obtaining_api_id).
|
||||
|
||||
|
||||
### Setup
|
||||
```sh
|
||||
cp .env.example .env
|
||||
# edit the .env file
|
||||
source .env
|
||||
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install kurigram
|
||||
|
||||
python3 generate_session.py
|
||||
|
||||
docker compose up -d --build
|
||||
```
|
||||
9
compose.yml
Normal file
9
compose.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
services:
|
||||
pacer:
|
||||
build: .
|
||||
env_file: .env
|
||||
environment:
|
||||
TZ: Europe/Moscow
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./user.session:/app/user.session
|
||||
1
crontab
Normal file
1
crontab
Normal file
|
|
@ -0,0 +1 @@
|
|||
39 14 * * 1,4 python3 /app/main.py
|
||||
14
generate_session.py
Normal file
14
generate_session.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import os
|
||||
import asyncio
|
||||
|
||||
import pyrogram
|
||||
|
||||
|
||||
async def main():
|
||||
c = pyrogram.client.Client('user', api_id=os.environ['TELEGRAM_API_ID'], api_hash=os.environ['TELEGRAM_API_HASH'])
|
||||
me = await c.get_me()
|
||||
print(f'Hello, {me.first_name}!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
337
main.py
Normal file
337
main.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json as _json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from typing import Literal
|
||||
from datetime import datetime as Dt, timedelta as Td
|
||||
import urllib.parse as up
|
||||
|
||||
import pyrogram
|
||||
import requests
|
||||
|
||||
FETCH_DELAY = 50
|
||||
UPDATE_DELAY = 30
|
||||
|
||||
|
||||
type PrimitiveType = int | str | bool | float | bytes | None
|
||||
type ValidData = PrimitiveType | list[ValidData] | dict[str, ValidData]
|
||||
|
||||
|
||||
jq = lambda x: subprocess.run('jq', input=_json.dumps(x).encode())
|
||||
|
||||
def print_headers(r: requests.Response):
|
||||
for k, v in r.request.headers.items():
|
||||
print(f'{k}: {v}')
|
||||
print(r.request.body)
|
||||
|
||||
def print_request(r: requests.Response):
|
||||
print(r.url)
|
||||
print_headers(r)
|
||||
if str(r.headers.get('content-type')).startswith('application/json'):
|
||||
print(r.status_code)
|
||||
jq(r.json())
|
||||
else:
|
||||
print(r.status_code, r.text)
|
||||
r.raise_for_status()
|
||||
|
||||
|
||||
class PacerClient:
|
||||
SECRET_UUID = b'B7A4DB15-D69A-4C8A-BA68-39E0AA208DB8'
|
||||
BASE_URL = 'https://api.pacer.cc/'
|
||||
|
||||
|
||||
def __init__(self, email: str, password: str):
|
||||
self._session = requests.Session()
|
||||
self._device_id = self.random_device_id()
|
||||
self._authorize(email, password)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def random_device_id() -> str:
|
||||
return ''.join(random.choice('0123456789abcdef') for _ in range(16))
|
||||
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: Literal['GET', 'POST', 'PUT'],
|
||||
path: str,
|
||||
account_id: int,
|
||||
access_token: str | None,
|
||||
params: dict[str, PrimitiveType] | None = None,
|
||||
data: dict[str, PrimitiveType] | None = None,
|
||||
json: ValidData | None = None,
|
||||
):
|
||||
data_hash = params_url = content_type = None
|
||||
|
||||
if params:
|
||||
for k, v in params.items():
|
||||
if isinstance(v, bool):
|
||||
params[k] = str(v).lower()
|
||||
params_url = up.urlencode(params) if params else None
|
||||
|
||||
if data:
|
||||
content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
data_hash = self._md5(up.urlencode(data))
|
||||
|
||||
if json:
|
||||
content_type = 'application/json; charset=UTF-8'
|
||||
data_hash = self._md5(_json.dumps(json, separators=(',', ':')))
|
||||
|
||||
|
||||
nonce = str(random.randrange(1000000000))
|
||||
timestamp = str(int(time.time()))
|
||||
|
||||
auth_header = 'Pacer ' + self._generate_signature(
|
||||
access_token=access_token,
|
||||
body_hash=data_hash,
|
||||
current_time=timestamp,
|
||||
nonce=nonce,
|
||||
params=params_url,
|
||||
path='/' + path
|
||||
)
|
||||
|
||||
r = requests.request(
|
||||
method=method,
|
||||
params=params,
|
||||
url=up.urljoin(self.BASE_URL, path),
|
||||
headers={
|
||||
'Authorization': auth_header,
|
||||
'X-Pacer-Nonce': nonce,
|
||||
'X-Pacer-Time': timestamp,
|
||||
'User-Agent': 'okhttp/4.10.0',
|
||||
'X-Pacer-Access-Token': access_token or '',
|
||||
'X-Pacer-Request-Token': 'true' if not access_token else None,
|
||||
'Content-Type': content_type,
|
||||
'X-Pacer-Device-Id': self._device_id,
|
||||
'X-Pacer-Account-Id': str(account_id),
|
||||
'X-Pacer-OS': 'android',
|
||||
'X-Pacer-Product': 'pacer',
|
||||
'X-Pacer-Client-Id': 'pacer_android',
|
||||
'X-Pacer-Language': 'en',
|
||||
'X-Pacer-Locale': 'en_US',
|
||||
'X-Pacer-Timezone': 'Europe/Moscow',
|
||||
'X-Pacer-Version': 'p12.10.1',
|
||||
'X-Pacer-Timezone-Offset': '180',
|
||||
},
|
||||
data=_json.dumps(json, separators=(',', ':')) if json else data,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _md5(s: str) -> str:
|
||||
md5 = hashlib.md5()
|
||||
md5.update(s.encode())
|
||||
|
||||
digest = md5.digest()
|
||||
return ''.join(f'{b:02x}' for b in digest)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _generate_signature(
|
||||
cls,
|
||||
path: str,
|
||||
current_time: str,
|
||||
nonce: str,
|
||||
access_token: str | None = None,
|
||||
params: str | None = None,
|
||||
body_hash: str | None = None
|
||||
) -> str:
|
||||
mac = hmac.new(cls.SECRET_UUID, digestmod=hashlib.sha1)
|
||||
|
||||
mac.update(current_time.encode())
|
||||
mac.update(nonce.encode())
|
||||
|
||||
if access_token:
|
||||
mac.update(access_token.encode())
|
||||
|
||||
if params:
|
||||
mac.update(params.encode())
|
||||
|
||||
if body_hash:
|
||||
mac.update(body_hash.encode())
|
||||
|
||||
if path:
|
||||
mac.update(path.encode())
|
||||
|
||||
digest = mac.digest()
|
||||
b64 = base64.b64encode(digest).decode()
|
||||
return up.quote(b64, safe='').replace('%0A', '')
|
||||
|
||||
|
||||
def _authorize(self, email: str, password: str):
|
||||
r = self.request('POST', 'api/v2.0/accounts', 0, None)
|
||||
data = r.json()['data']
|
||||
access_token = data['access_token']
|
||||
account_id = data['id']
|
||||
|
||||
r = self.request(
|
||||
method='POST',
|
||||
path='api/v2.0/login',
|
||||
account_id=account_id,
|
||||
access_token=access_token,
|
||||
json={
|
||||
'email': email,
|
||||
'password': self._md5(password),
|
||||
}
|
||||
)
|
||||
|
||||
data = r.json()['data']
|
||||
self.access_token: str = data['access_token']
|
||||
self.account_id: int = data['id']
|
||||
|
||||
|
||||
def make_run(self, distance: float, duration: int, calories: float, steps: int) -> str:
|
||||
current_time = Dt.now()
|
||||
current_timestamp = int(current_time.timestamp())
|
||||
start_time = current_time - Td(seconds=duration)
|
||||
client_hash = f"PID{self.account_id}--{uuid.uuid4()!s}"
|
||||
|
||||
r = self.request(
|
||||
method='PUT',
|
||||
path=f'api/v2.0/accounts/me/activities/daily_summaries/{current_time.strftime("%y%m%d")}',
|
||||
account_id=self.account_id,
|
||||
access_token=self.access_token,
|
||||
params={'post_note': 'false'},
|
||||
json={
|
||||
"calories": 1,
|
||||
"client_hash": "auto",
|
||||
"client_timezone": "Europe/Moscow",
|
||||
"client_timezone_offset": 180,
|
||||
"client_unixtime": current_timestamp,
|
||||
"data_version": 1,
|
||||
"distance_value": 1.0,
|
||||
"duration_in_seconds": 1,
|
||||
"floors": 0,
|
||||
"is_background": False,
|
||||
"last_foreground_unixtime": current_timestamp,
|
||||
"pedometer_mode": "111",
|
||||
"recorded_by": "phone",
|
||||
"recorded_for_datetime_iso8601": start_time.strftime("%Y-%m-%dT00:00:00.000+03:00"),
|
||||
"sessions": [{
|
||||
"calories": calories,
|
||||
"client_hash": client_hash,
|
||||
"client_payload": "{\"is_normal_data\":false,\"storageType\":\"trackTable\",\"trackId\":1,\"trackLogType\":\"Gps_Normally_Finished\"}",
|
||||
"client_timezone": "Europe/Moscow",
|
||||
"client_timezone_offset": 180,
|
||||
"client_unixtime": current_timestamp,
|
||||
"data_version": 1,
|
||||
"deleted": False,
|
||||
"distance_value": distance,
|
||||
"duration_in_seconds": duration,
|
||||
"end_for_unixtime": current_timestamp,
|
||||
"google_fit_sync_state": "unsync",
|
||||
"partner_session_type": "",
|
||||
"partner_sync_hash": 0,
|
||||
"partner_sync_state": "unsync",
|
||||
"recorded_by": "phone",
|
||||
"recorded_for_datetime_iso8601": start_time.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
|
||||
"steps": steps,
|
||||
"type": 1002
|
||||
}],
|
||||
"steps": 0,
|
||||
"type": 0,
|
||||
}
|
||||
)
|
||||
|
||||
r = self.request(
|
||||
method='POST',
|
||||
path='pacer/android/api/v18/track/upload',
|
||||
account_id=self.account_id,
|
||||
access_token=self.access_token,
|
||||
params={'post_note': 'false'},
|
||||
data={
|
||||
'metadata': _json.dumps({
|
||||
"visible": "public",
|
||||
"account_id": str(self.account_id),
|
||||
"activity_data": {
|
||||
"calories": calories,
|
||||
"distance": distance,
|
||||
"duration": duration,
|
||||
"source": "pacer_android",
|
||||
"steps": steps,
|
||||
},
|
||||
"client_hash": client_hash,
|
||||
"description": "",
|
||||
"hide_map": False,
|
||||
"share_url": "",
|
||||
"start_time": int(start_time.timestamp()),
|
||||
"title": "bebra",
|
||||
"track_data": "",
|
||||
"track_id": "",
|
||||
"type": "run",
|
||||
"visible": "global",
|
||||
}, separators=(',', ':'))
|
||||
}
|
||||
)
|
||||
|
||||
return r.json()['data']['share_url']
|
||||
|
||||
|
||||
async def update_runs(delay: float | int = 5):
|
||||
c = pyrogram.client.Client('user')
|
||||
await c.start()
|
||||
|
||||
m = await c.send_message('@KronbarsRunningBot', '/profile')
|
||||
id: int = m.chat.id # type: ignore
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
h = [x async for x in c.get_chat_history('@KronbarsRunningBot', limit=1)][0]
|
||||
mid = h.id
|
||||
await c.request_callback_answer(id, mid, 'trackers')
|
||||
await asyncio.sleep(delay)
|
||||
await c.request_callback_answer(id, mid, 'tracker-profile_pacer')
|
||||
await asyncio.sleep(delay)
|
||||
await c.request_callback_answer(id, mid, 'update-tracker-activities_pacer')
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
|
||||
async def main():
|
||||
email = os.getenv('EMAIL')
|
||||
password = os.getenv('PASSWORD')
|
||||
|
||||
if not (email and password):
|
||||
raise ValueError('Missing email or password!')
|
||||
|
||||
print('-- New run: ', Dt.now().strftime('%a %m/%d %I:%M %P'), '--')
|
||||
|
||||
calories = float(os.getenv('CALORIES') or 0) or 383.0 + (random.random() * 50)
|
||||
distance = float(os.getenv('DISTANCE') or 0) or 5239.0 + (random.random() * 30)
|
||||
duration = int(os.getenv('DURATION') or '0') or 2039 + int(random.random() * 30)
|
||||
steps = int(os.getenv('STEPS') or '0') or 6239 + int(random.random() * 130)
|
||||
|
||||
url = PacerClient(email, password).make_run(
|
||||
calories=calories,
|
||||
distance=distance,
|
||||
duration=duration,
|
||||
steps=steps,
|
||||
)
|
||||
|
||||
print(f'Calories: {calories}')
|
||||
print(f'Distance: {distance}')
|
||||
print(f'Duration: {duration}')
|
||||
print(f'Steps: {steps}')
|
||||
print(f'Share: {url}')
|
||||
|
||||
if not os.getenv("TELEGRAM_SKIP"):
|
||||
print(f'-- Sleeping for {FETCH_DELAY} minutes before fetching runs... --')
|
||||
await asyncio.sleep(FETCH_DELAY * 60)
|
||||
print("-- Fetching runs --")
|
||||
try:
|
||||
await update_runs(UPDATE_DELAY)
|
||||
except Exception as e:
|
||||
print(f'Failed to fetch runs: {e}')
|
||||
|
||||
print('-- Done! --')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
kurigram==2.2.13
|
||||
requests>=2.32.5
|
||||
Loading…
Add table
Add a link
Reference in a new issue