replace get-twitch-token bash script with cross-platform Dart script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26d26a9554
commit
fd4da6a393
3 changed files with 153 additions and 100 deletions
|
|
@ -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.
|
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]
|
> [!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.
|
> 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.
|
||||||
|
|
|
||||||
|
|
@ -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."
|
|
||||||
152
scripts/get_twitch_token.dart
Normal file
152
scripts/get_twitch_token.dart
Normal file
|
|
@ -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<String, dynamic>;
|
||||||
|
|
||||||
|
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<String, String> 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<String, String> _loadEnvFile() {
|
||||||
|
final scriptDir = File(Platform.script.toFilePath()).parent;
|
||||||
|
final projectDir = scriptDir.parent;
|
||||||
|
final envFile = File('${projectDir.path}/.env');
|
||||||
|
|
||||||
|
if (!envFile.existsSync()) return {};
|
||||||
|
|
||||||
|
final result = <String, String>{};
|
||||||
|
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<void> _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue