From fd4da6a39323152715e3c124b8790bb409bdc35a Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sat, 7 Mar 2026 10:53:07 -0800 Subject: [PATCH] replace get-twitch-token bash script with cross-platform Dart script Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- scripts/get-twitch-token.sh | 99 ---------------------- scripts/get_twitch_token.dart | 152 ++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 100 deletions(-) delete mode 100755 scripts/get-twitch-token.sh create mode 100644 scripts/get_twitch_token.dart diff --git a/README.md b/README.md index 920cda4d..834bbc44 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ For a more detailed overview, visit [frostyapp.io](https://www.frostyapp.io/). 6. Choose an emulator or device and run the app! The included VS Code launch configurations will automatically read your `.env` file. -7. (Optional) Run `./scripts/get-twitch-token.sh` to quickly get a user auth token. Long-press the **Anonymous** account tile in the app to log in from your clipboard. +7. (Optional) Run `dart run scripts/get_twitch_token.dart` to quickly get a user auth token. Long-press the **Anonymous** account tile in the app to log in. > [!IMPORTANT] > Frosty uses [MobX](https://mobx.netlify.app/) for state management. Please refer to the documentation about code generation, otherwise your changes within MobX stores may not be applied. diff --git a/scripts/get-twitch-token.sh b/scripts/get-twitch-token.sh deleted file mode 100755 index 9f6eb193..00000000 --- a/scripts/get-twitch-token.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -# -# Gets a Twitch OAuth user token for emulator testing. -# -# Usage: ./scripts/get-twitch-token.sh -# -# Reads CLIENT_ID and SECRET from .env, env vars, or prompts interactively. -# Opens the Twitch OAuth page in your browser — after you authorize, -# paste the redirect URL back here and the token is copied to your clipboard. -# Then long-press the Anonymous account tile in the app to log in. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -ENV_FILE="$PROJECT_DIR/.env" - -load_env_var() { - local name="$1" - local current="${!name:-}" - - if [ -n "$current" ]; then - return - fi - - if [ -f "$ENV_FILE" ] && grep -q "^${name}=" "$ENV_FILE"; then - eval "$name=$(grep "^${name}=" "$ENV_FILE" | cut -d= -f2-)" - return - fi - - read -rp "$name: " "$name" -} - -load_env_var CLIENT_ID -load_env_var SECRET - -if [ -z "${CLIENT_ID:-}" ] || [ -z "${SECRET:-}" ]; then - echo "Error: CLIENT_ID and SECRET are both required." - echo "Add them to .env or export them in your shell." - exit 1 -fi - -SCOPES="chat:read+chat:edit+user:read:follows+user:read:blocked_users+user:manage:blocked_users+user:manage:chat_color" -REDIRECT_URI="https://twitch.tv/login" - -URL="https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${SCOPES}&force_verify=true" - -echo "Opening Twitch authorization in your browser..." -case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) start "$URL" ;; - Darwin*) open "$URL" ;; - *) xdg-open "$URL" ;; -esac - -echo "" -echo "After authorizing, copy the full URL from the address bar and paste it here:" -echo "(it will look like https://twitch.tv/login?code=...)" -echo "" -read -rp "> " REDIRECT_URL - -# Extract the authorization code from the redirect URL query params -CODE=$(echo "$REDIRECT_URL" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p') - -if [ -z "$CODE" ]; then - echo "" - echo "Error: could not extract code from that URL." - echo "Make sure you pasted the full URL including the ?code=... part." - exit 1 -fi - -# Exchange the authorization code for an access token -echo "" -echo "Exchanging code for token..." - -RESPONSE=$(curl -s -X POST "https://id.twitch.tv/oauth2/token" \ - -d "client_id=${CLIENT_ID}" \ - -d "client_secret=${SECRET}" \ - -d "code=${CODE}" \ - -d "grant_type=authorization_code" \ - -d "redirect_uri=${REDIRECT_URI}") - -# Try to extract access_token from JSON response -TOKEN=$(echo "$RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) - -if [ -z "$TOKEN" ]; then - echo "Error: token exchange failed." - echo "$RESPONSE" - exit 1 -fi - -case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) echo "$TOKEN" | tr -d '\n' | clip ;; - Darwin*) echo "$TOKEN" | tr -d '\n' | pbcopy ;; - *) echo "$TOKEN" | tr -d '\n' | xclip -selection clipboard ;; -esac - -echo "Token copied to clipboard!" -echo "" -echo "Now in the emulator, long-press the Anonymous account tile to log in." diff --git a/scripts/get_twitch_token.dart b/scripts/get_twitch_token.dart new file mode 100644 index 00000000..cca94104 --- /dev/null +++ b/scripts/get_twitch_token.dart @@ -0,0 +1,152 @@ +// Gets a Twitch OAuth user token for emulator testing. +// +// Usage: dart run scripts/get_twitch_token.dart +// +// Reads CLIENT_ID and SECRET from .env, env vars, or prompts interactively. +// Opens the Twitch OAuth page in your browser — after you authorize, +// paste the redirect URL back here and the token is printed. +// Then long-press the Anonymous account tile in the app to log in. +import 'dart:convert'; +import 'dart:io'; + +const scopes = + 'chat:read+chat:edit+user:read:follows+user:read:blocked_users+' + 'user:manage:blocked_users+user:manage:chat_color'; +const redirectUri = 'https://twitch.tv/login'; + +void main() async { + final env = _loadEnvFile(); + + final clientId = _resolve('CLIENT_ID', env); + final secret = _resolve('SECRET', env); + + if (clientId == null || secret == null) { + stderr.writeln('Error: CLIENT_ID and SECRET are both required.'); + stderr.writeln('Add them to .env or export them in your shell.'); + exit(1); + } + + final authorizeUrl = 'https://id.twitch.tv/oauth2/authorize' + '?client_id=$clientId' + '&redirect_uri=$redirectUri' + '&response_type=code' + '&scope=$scopes' + '&force_verify=true'; + + stdout.writeln('Opening Twitch authorization in your browser...'); + await _openUrl(authorizeUrl); + + stdout.writeln(); + stdout.writeln( + 'After authorizing, copy the full URL from the address bar and paste it here:', + ); + stdout.writeln('(it will look like https://twitch.tv/login?code=...)'); + stdout.writeln(); + stdout.write('> '); + + final redirectUrl = stdin.readLineSync()?.trim(); + if (redirectUrl == null || redirectUrl.isEmpty) { + stderr.writeln('No URL provided.'); + exit(1); + } + + final uri = Uri.tryParse(redirectUrl); + final code = uri?.queryParameters['code']; + + if (code == null || code.isEmpty) { + stderr.writeln(); + stderr.writeln('Error: could not extract code from that URL.'); + stderr.writeln( + 'Make sure you pasted the full URL including the ?code=... part.', + ); + exit(1); + } + + stdout.writeln(); + stdout.writeln('Exchanging code for token...'); + + final client = HttpClient(); + try { + final request = await client.postUrl( + Uri.parse('https://id.twitch.tv/oauth2/token'), + ); + request.headers.contentType = ContentType( + 'application', + 'x-www-form-urlencoded', + ); + request.write( + 'client_id=$clientId' + '&client_secret=$secret' + '&code=$code' + '&grant_type=authorization_code' + '&redirect_uri=$redirectUri', + ); + + final response = await request.close(); + final body = await response.transform(utf8.decoder).join(); + final json = jsonDecode(body) as Map; + + final token = json['access_token'] as String?; + if (token == null || token.isEmpty) { + stderr.writeln('Error: token exchange failed.'); + stderr.writeln(body); + exit(1); + } + + stdout.writeln(); + stdout.writeln('Token: $token'); + stdout.writeln(); + stdout.writeln( + 'Now in the emulator, long-press the Anonymous account tile to log in.', + ); + } finally { + client.close(); + } +} + +/// Reads a value from env vars first, then .env file, then prompts. +String? _resolve(String name, Map envFile) { + final fromEnv = Platform.environment[name]; + if (fromEnv != null && fromEnv.isNotEmpty) return fromEnv; + + final fromFile = envFile[name]; + if (fromFile != null && fromFile.isNotEmpty) return fromFile; + + stdout.write('$name: '); + return stdin.readLineSync()?.trim(); +} + +/// Parses the .env file next to this script's project root. +Map _loadEnvFile() { + final scriptDir = File(Platform.script.toFilePath()).parent; + final projectDir = scriptDir.parent; + final envFile = File('${projectDir.path}/.env'); + + if (!envFile.existsSync()) return {}; + + final result = {}; + for (final line in envFile.readAsLinesSync()) { + final trimmed = line.trim(); + if (trimmed.isEmpty || trimmed.startsWith('#')) continue; + final eq = trimmed.indexOf('='); + if (eq < 0) continue; + result[trimmed.substring(0, eq)] = trimmed.substring(eq + 1); + } + return result; +} + +/// Opens a URL in the default browser across platforms. +Future _openUrl(String url) async { + final ProcessResult result; + if (Platform.isWindows) { + result = await Process.run('cmd', ['/c', 'start', '', url]); + } else if (Platform.isMacOS) { + result = await Process.run('open', [url]); + } else { + result = await Process.run('xdg-open', [url]); + } + if (result.exitCode != 0) { + stderr.writeln('Could not open browser. Visit this URL manually:'); + stderr.writeln(url); + } +}