From a9cfc9b522a9396db8e1241a901fc4beb770ca9e Mon Sep 17 00:00:00 2001 From: "Arthur K." Date: Tue, 4 Nov 2025 07:59:41 +0300 Subject: [PATCH] Add files via upload :trollface: --- .env.example | 16 +++ .gitignore | 4 + Dockerfile | 16 +++ README.md | 26 ++++ compose.yml | 9 ++ crontab | 1 + generate_session.py | 14 ++ main.py | 337 ++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 9 files changed, 425 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 compose.yml create mode 100644 crontab create mode 100644 generate_session.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64cf0ce --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4247af7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env* +!.env.example +*.session +.venv diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3450c80 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a64991 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..db47c90 --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + pacer: + build: . + env_file: .env + environment: + TZ: Europe/Moscow + restart: unless-stopped + volumes: + - ./user.session:/app/user.session diff --git a/crontab b/crontab new file mode 100644 index 0000000..b8370c9 --- /dev/null +++ b/crontab @@ -0,0 +1 @@ +39 14 * * 1,4 python3 /app/main.py diff --git a/generate_session.py b/generate_session.py new file mode 100644 index 0000000..e252af7 --- /dev/null +++ b/generate_session.py @@ -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()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..8fbd886 --- /dev/null +++ b/main.py @@ -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()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8977420 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +kurigram==2.2.13 +requests>=2.32.5