Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 17 additions & 35 deletions packages/cli/bin/cli.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:cli/caches.dart';
import 'package:cli/api.dart';
import 'package:cli/config.dart';
import 'package:cli/logger.dart';
import 'package:cli/logic/logic.dart';
Expand Down Expand Up @@ -43,37 +43,6 @@ void printRequestStats(RequestCounts requestCounts, Duration duration) {
..info('Used $percentString of $possible possible requests.');
}

Future<String> getAgentToken(Database db, {required Uri baseUri}) async {
final agentToken = await db.config.getAgentToken();
final accountToken = await db.global.getAccountToken();
if (agentToken == null && accountToken == null) {
throw StateError('No agent or account token found.');
}
// First check if we have an agent token
if (agentToken != null) {
// The token might be invalid, but further callers will handle that.
return agentToken;
}

final agentSymbolFromEnv = Platform.environment['ST_AGENT'];
if (agentSymbolFromEnv == null) {
throw StateError('No agent symbol found, cannot register new agent.');
}
// Otherwise, register a new user.
final token = await register(
db,
agentSymbol: agentSymbolFromEnv,
baseUri: baseUri,
);
await db.config.setAgentToken(token);
return token;
}

Future<Api> getApi(Database db, {required Uri baseUri}) async {
final agentToken = await getAgentToken(db, baseUri: baseUri);
return apiFromAuthToken(agentToken, db, baseUri: baseUri);
}

Future<void> cliMain(List<String> args) async {
final parser = ArgParser()
..addFlag('verbose', abbr: 'v', negatable: false, help: 'Verbose logging.')
Expand All @@ -90,18 +59,31 @@ Future<void> cliMain(List<String> args) async {
final start = DateTime.timestamp();

logger.info('Welcome to Space Traders! 🚀');
final agentSymbol = Platform.environment['ST_AGENT'];

final db = await defaultDatabase();
final baseUri = await determineBaseUri(db);
final api = await getApi(db, baseUri: baseUri);
// Explicitly get an apiClient so we have it typed as AuthorizedClient.
final apiClient = await getApiClient(db, baseUri: baseUri);
apiClient
..accountToken = await db.global.getAccountToken()
// accountToken must be set first since registerAgentIfNeeded might call
// register which would use the accountToken.
..agentToken = await registerAgentIfNeeded(
db,
baseUri: baseUri,
agentSymbol: agentSymbol,
);

final api = Api(apiClient);

// Handle ctrl-c and print out request stats.
ProcessSignal.sigint.watch().listen((signal) {
final duration = DateTime.timestamp().difference(start);
printRequestStats(api.requestCounts, duration);
exit(0);
});

await enterReset(api, db, baseUri);
await reregisterLoop(api, db, baseUri, agentSymbol: agentSymbol);
}

Future<void> main(List<String> args) async {
Expand Down
21 changes: 13 additions & 8 deletions packages/cli/bin/idle_queue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import 'package:cli/logic/systems_fetcher.dart';
import 'package:cli/net/auth.dart';
import 'package:cli/net/queries.dart';

Future<T> waitFor<T>(Database db, Future<T?> Function() get) async {
var value = await get();
while (value == null) {
logger.info('$T not yet in database, waiting 1 minute.');
await Future<void>.delayed(const Duration(minutes: 1));
value = await get();
}
return value;
/// Waits for the auth token to be available and then creates an API.
Future<Api> waitForApi(
Database db, {
required Uri baseUri,
int Function() getPriority = defaultGetPriority,
}) async {
final token = await waitFor(db, db.config.getAgentToken, name: 'agent token');
return await apiFromAgentToken(
token,
db,
baseUri: baseUri,
getPriority: getPriority,
);
}

Future<void> command(Database db, ArgResults argResults) async {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/lib/api.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:cli/net/counts.dart';
import 'package:cli/net/auth.dart';
import 'package:http/http.dart' as http;
import 'package:types/types.dart';

Expand All @@ -20,7 +20,7 @@ class Api {
factions = FactionsApi(apiClient);

/// The shared ApiClient.
final CountingApiClient apiClient;
final AuthorizedClient apiClient;

/// Counts of requests sent through this api.
RequestCounts get requestCounts => apiClient.requestCounts;
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/lib/cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ Future<void> runOffline(
);
}

// Unclear where this should live.
/// Waits for a value to be available in the database.
Future<T> waitFor<T>(
Database db,
Future<T?> Function() get, {
String? name,
}) async {
var value = await get();
while (value == null) {
final nameString = name ?? '$T';
logger.info('$nameString not yet in database, waiting 1 minute.');
await Future<void>.delayed(const Duration(minutes: 1));
value = await get();
}
return value;
}

/// Common lookups which CLIs might need.

/// Get the symbol of the agent's headquarters.
Expand Down
37 changes: 30 additions & 7 deletions packages/cli/lib/logic/logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:cli/logic/printing.dart';
import 'package:cli/logic/ship_waiter.dart';
import 'package:cli/net/exceptions.dart';
import 'package:cli/net/queries.dart';
import 'package:cli/net/register.dart';
import 'package:db/db.dart';
import 'package:types/types.dart';

Expand Down Expand Up @@ -207,13 +208,7 @@ Future<Never> logic(
await Future<void>.delayed(const Duration(minutes: 1));
continue;
}

// Need to handle token changes after reset.
// ApiException 401: {"error":{"message":"Failed to parse token.
// Token reset_date does not match the server. Server resets happen on a
// weekly to bi-weekly frequency during alpha. After a reset, you should
// re-register your agent. Expected: 2023-06-03, Actual: 2023-05-20",
// "code":401,"data":{"expected":"2023-06-03","actual":"2023-05-20"}}}
// Caller will handle token changes after reset.
rethrow;
}
}
Expand Down Expand Up @@ -329,3 +324,31 @@ Future<void> enterReset(Api api, Database db, Uri baseUri) async {

await logic(api, db, centralCommand, caches);
}

/// Run the agent loop forever, re-registering a new agent if necessary.
Future<void> reregisterLoop(
Api api,
Database db,
Uri baseUri, {
required String? agentSymbol,
}) async {
while (true) {
try {
await enterReset(api, db, baseUri);
} on ApiException catch (e) {
if (isTokenMismatchException(e)) {
logger.warn('Token mismatch, re-registering agent.');
await db.config.clearAgentToken();
final agentToken = await registerAgentIfNeeded(
db,
baseUri: baseUri,
agentSymbol: agentSymbol,
);
api.apiClient.agentToken = agentToken;
await db.config.setAgentToken(agentToken);
continue;
}
rethrow;
}
}
}
59 changes: 30 additions & 29 deletions packages/cli/lib/net/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ export 'package:cli/net/queue.dart'
/// Default priority function.
int defaultGetPriority() => networkPriorityDefault;

/// An api client that is authorized with an agent or account token.
class AuthorizedClient extends CountingApiClient {
/// Construct an authorized client.
AuthorizedClient({super.baseUri, super.client});

/// The agent token.
String? agentToken;

/// The account token.
String? accountToken;

/// Get the secret from the client.
String? getSecret(String name) {
if (name == 'AgentToken') {
return agentToken;
}
if (name == 'AccountToken') {
return accountToken;
}
return null;
}
}

/// Gets the base uri to use for the api client.
Future<Uri> determineBaseUri(Database db) async {
// If we have an environment variable, return that.
Expand All @@ -36,53 +59,31 @@ QueuedClient getQueuedClient(
}

/// Create an API client with priority function.
Future<CountingApiClient> getApiClient(
Future<AuthorizedClient> getApiClient(
Database db, {
required Uri baseUri,
int Function() getPriority = defaultGetPriority,
Map<String, String>? defaultHeaders,
}) async {
return CountingApiClient(
defaultHeaders: defaultHeaders ?? {},
return AuthorizedClient(
client: getQueuedClient(db, getPriority: getPriority),
baseUri: baseUri,
);
}

/// apiFromAuthToken creates an Api with the given auth token.
Future<Api> apiFromAuthToken(
/// apiFromAgentToken creates an Api with the given agent token.
Future<Api> apiFromAgentToken(
String token,
Database db, {
required Uri baseUri,
int Function() getPriority = defaultGetPriority,
}) async {
final defaultHeaders = <String, String>{'Authorization': 'Bearer $token'};
final apiClient = await getApiClient(
db,
getPriority: getPriority,
defaultHeaders: defaultHeaders,
baseUri: baseUri,
);
return Api(apiClient);
}

/// Waits for the auth token to be available and then creates an API.
Future<Api> waitForApi(
Database db, {
required Uri baseUri,
int Function() getPriority = defaultGetPriority,
}) async {
var token = await db.config.getAgentToken();
while (token == null) {
await Future<void>.delayed(const Duration(minutes: 1));
token = await db.config.getAgentToken();
}
return apiFromAuthToken(
token,
db,
getPriority: getPriority,
baseUri: baseUri,
);
apiClient.agentToken = token;
return Api(apiClient);
}

/// defaultApi creates an Api with the default auth token read from the
Expand All @@ -96,7 +97,7 @@ Future<Api> defaultApi(
if (token == null) {
throw Exception('No auth token found.');
}
return apiFromAuthToken(
return apiFromAgentToken(
token,
db,
getPriority: getPriority,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/lib/net/counts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:types/types.dart';
/// ApiClient that counts the number of requests made.
class CountingApiClient extends ApiClient {
/// Construct a rate limited api client.
CountingApiClient({super.baseUri, super.client, super.defaultHeaders});
CountingApiClient({super.baseUri, super.client});

/// RequestCounts tracks the number of requests made to each path.
final RequestCounts requestCounts = RequestCounts();
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/lib/net/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ bool isInsufficientFuelException(ApiException e) {
return isAPIExceptionWithCode(e, 4203);
}

// ApiException 401: {"error":{"message":"Failed to parse token.
// Token reset_date does not match the server. Server resets happen on a
// weekly to bi-weekly frequency during alpha. After a reset, you should
// re-register your agent. Expected: 2023-06-03, Actual: 2023-05-20",
// "code":401,"data":{"expected":"2023-06-03","actual":"2023-05-20"}}}
/// Returns true if the exception is a token mismatch exception.
bool isTokenMismatchException(ApiException e) {
return isAPIExceptionWithCode(e, 401);
}

// ApiException 503: {"error":{"message":"SpaceTraders is currently in
// maintenance mode and unavailable. This will hopefully only last a few
// minutes while we update or upgrade our servers. Check discord for updates
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/lib/net/register.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,29 @@ Future<String> register(
final registerResponse = await AccountsApi(client).register(registerRequest);
return registerResponse.data.token;
}

/// Register a new agent if needed and return the agent token.
Future<String> registerAgentIfNeeded(
Database db, {
required Uri baseUri,
required String? agentSymbol,
}) async {
final agentToken = await db.config.getAgentToken();
final accountToken = await db.global.getAccountToken();
if (agentToken == null && accountToken == null) {
throw StateError('No agent or account token found.');
}
// First check if we have an agent token
if (agentToken != null) {
// The token might be invalid, but further callers will handle that.
return agentToken;
}

if (agentSymbol == null) {
throw StateError('No agent symbol found, cannot register new agent.');
}
// Otherwise, register a new user.
final token = await register(db, agentSymbol: agentSymbol, baseUri: baseUri);
await db.config.setAgentToken(token);
return token;
}
5 changes: 5 additions & 0 deletions packages/db/lib/src/stores/config_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ class ConfigStore {
Future<void> setAgentToken(String token) async {
await _setString('agent_token', token);
}

/// Clear the agent token from the config table in the db.
Future<void> clearAgentToken() async {
await _db.executeSql("DELETE FROM config_ WHERE key = 'agent_token'");
}
}