Skip to content

Commit

Permalink
Add AI domain brainstorming
Browse files Browse the repository at this point in the history
  • Loading branch information
breitburg committed Jul 25, 2023
1 parent 68c729c commit 113cf03
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 89 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ If you'd like to contribute directly to the codebase, you can follow these steps
1. Fork the repository and clone it to your local machine.
2. Create a new branch for your feature or bug fix: `git checkout -b my-branch`.
3. Make the necessary changes and additions.
4. Run the tests to ensure they pass: `dart test`.
5. Commit your changes: `git commit -m "Add feature or bug fix"`.
6. Push to your branch: `git push origin my-branch`.
7. Open a pull request on the main repository.
4. Commit your changes: `git commit -m "Add feature or bug fix"`.
5. Push to your branch: `git push origin my-branch`.
6. Open a pull request on the main repository.

Please ensure that your contributions align with the project's coding style and guidelines. Your involvement helps improve Domine for everyone.
5 changes: 4 additions & 1 deletion bin/domine.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:args/command_runner.dart';
import 'package:domine/commands/brainstorm.dart';
import 'package:domine/commands/check.dart';

void main(List<String> arguments) async {
Expand All @@ -7,6 +8,8 @@ void main(List<String> arguments) async {
'Search and purchase domains right from the terminal.',
);

runner.addCommand(SearchCommand());
runner.addCommand(CheckCommand());
runner.addCommand(BrainstormCommand());

runner.run(arguments);
}
78 changes: 78 additions & 0 deletions lib/checker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'dart:convert';

import 'package:domine/constants.dart';
import 'package:domine/misc.dart';
import 'package:domine/models.dart';
import 'package:http/http.dart';

Future<List<CheckedDomain>> batchCheck(Iterable<String> input) async {
final futures = <Future>[];

for (final domain in input) {
for (final variant in expand(domain)) {
final parts = variant.split('.');
final name = parts.first;
final tld = parts.sublist(1).join('.');

futures.add(check(name, tlds: tld == '*' ? asteriskTLDs : [tld]));
}
}

final checks = await Future.wait(futures);

return [
for (final check in checks) ...[for (final domain in check) domain]
];
}

Future<List<CheckedDomain>> check(String name,
{required List<String> tlds}) async {
final parameters = {
'tlds': tlds.join(','),
'hash': hash(name, 27).toString(),
};

final dnsNamesReponse = await get(
Uri.https(
'instantdomainsearch.com',
'/services/dns-names/$name',
parameters,
),
);

final zoneNamesReponse = await get(
Uri.https(
'instantdomainsearch.com',
'/services/zone-names/$name',
parameters,
),
);

final decoded = [
if (zoneNamesReponse.body.isNotEmpty)
...zoneNamesReponse.body.trim().split('\n'),
if (dnsNamesReponse.body.isNotEmpty)
...dnsNamesReponse.body.trim().split('\n'),
].map((e) => jsonDecode(e));

return [
for (final check in decoded)
CheckedDomain(
name,
check['tld'],
status: switch (check['isRegistered']) {
true => CheckStatus.taken,
false => CheckStatus.available,
_ => CheckStatus.unknown,
},
)
];
}

int hash(String name, int t) {
for (int o = 0; o < name.length; o++) {
t = ((t << 5) - t + name.codeUnitAt(o)).toSigned(32);
}

return t;
}
110 changes: 110 additions & 0 deletions lib/commands/brainstorm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:cli_spinner/cli_spinner.dart';
import 'package:dart_openai/dart_openai.dart';
import 'package:domine/checker.dart';
import 'package:domine/misc.dart';
import 'package:domine/models.dart';
import 'package:tint/tint.dart';

class BrainstormCommand extends Command {
final List<CheckedDomain> _searches = [];

@override
String get description => 'Choose a domain with AI';

@override
String get name => 'brainstorm';

@override
List<String> get aliases => ['b'];

BrainstormCommand() {
argParser.addOption(
'openai-key',
abbr: 'k',
help: 'OpenAI API key',
);
argParser.addOption(
'model',
abbr: 'm',
help: 'GPT model',
defaultsTo: 'gpt-3.5-turbo',
);
}

@override
void run() async {
final results = argResults!;
OpenAI.apiKey = Platform.environment['OPENAI_KEY'] ?? results['openai-key'];

await _brainstorm(results.rest.join(' '), model: results['model']);
}

Future<void> _brainstorm(String prompt, {required String model}) async {
domainTable(_searches);

final spinner =
Spinner.type('Synthesizing domains with GPT...', SpinnerType.dots)
..start();

final response = await OpenAI.instance.chat.create(
model: model,
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.system,
content: [
'You are a creative AI that task is to find domains for your client.',
'Example queries: superbakery.com, nudes4sale.co',
'Domain search prompt: $prompt'
].join('\n'),
),
if (_searches.isNotEmpty)
OpenAIChatCompletionChoiceMessageModel(
functionName: 'checkDomains',
role: OpenAIChatMessageRole.function,
content: 'Already been checked: ${_searches.join(', ')}',
)
],
functionCall: FunctionCall.forFunction('checkDomains'),
functions: [
OpenAIFunctionModel.withParameters(
name: 'checkDomains',
parameters: [
OpenAIFunctionProperty.array(
name: 'queries',
description: 'Must be at least 10 queries',
items: OpenAIFunctionProperty.string(
name: 'domain',
description: 'Name and a TLD (example: google.com, nesper.co)',
),
)
],
),
],
);
final message = response.choices.first.message;
final queries =
List<String>.from(message.functionCall!.arguments!['queries'].where(
(e) =>
e.contains('.') &&
!e.endsWith('.') &&
!_searches.any((v) => v.toString() == e),
));

if (queries.isNotEmpty) {
spinner.updateMessage('Checking ${queries.length} domains...');
final checks = await batchCheck(queries);
_searches.addAll(checks);
}

spinner
..updateMessage('Currently, ${_searches.length} domains have been checked'
.dim()
.underline())
..stop();

await _brainstorm(prompt, model: model);
}
}
88 changes: 5 additions & 83 deletions lib/commands/check.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import 'dart:convert';

import 'package:args/command_runner.dart';
import 'package:cli_spinner/cli_spinner.dart';
import 'package:domine/constants.dart';
import 'package:domine/checker.dart';
import 'package:domine/misc.dart';
import 'package:domine/models.dart';
import 'package:http/http.dart';
import 'package:tint/tint.dart';

class SearchCommand extends Command {
class CheckCommand extends Command {
@override
String get description => 'Check domain(s) availability';

Expand All @@ -31,23 +27,8 @@ class SearchCommand extends Command {
final spinner = Spinner.type('Checking availability...', SpinnerType.dots)
..start();

final futures = <Future>[];

for (final domain in input) {
for (final variant in expand(domain)) {
final parts = variant.split('.');
final name = parts.first;
final tld = parts.sublist(1).join('.');

futures.add(check(name, tlds: tld == '*' ? asteriskTLDs : [tld]));
}
}

final checks = await Future.wait(futures);
final results = [
for (final check in checks) ...[for (final domain in check) domain]
]..sort();
final successes = results.where((e) => e.status == CheckStatus.available);
final results = await batchCheck(input);
final successes = results.where((e) => e.available);

spinner
..updateMessage(
Expand All @@ -59,65 +40,6 @@ class SearchCommand extends Command {
)
..stop();

table([
for (final domain in results)
'${(switch(domain.status) {
CheckStatus.available => '✔'.green(),
CheckStatus.taken => '⨯'.red(),
_ => '⁇'.blue(),
}).bold()} ${domain.name}.${domain.tld}'
]);
domainTable(results);
}
}

int hash(String name, int t) {
for (int o = 0; o < name.length; o++) {
t = ((t << 5) - t + name.codeUnitAt(o)).toSigned(32);
}

return t;
}

Future<List<CheckedDomain>> check(String name,
{required List<String> tlds}) async {
final parameters = {
'tlds': tlds.join(','),
'hash': hash(name, 27).toString(),
};

final dnsNamesReponse = await get(
Uri.https(
'instantdomainsearch.com',
'/services/dns-names/$name',
parameters,
),
);

final zoneNamesReponse = await get(
Uri.https(
'instantdomainsearch.com',
'/services/zone-names/$name',
parameters,
),
);

final decoded = [
if (zoneNamesReponse.body.isNotEmpty)
...zoneNamesReponse.body.trim().split('\n'),
if (dnsNamesReponse.body.isNotEmpty)
...dnsNamesReponse.body.trim().split('\n'),
].map((e) => jsonDecode(e));

return [
for (final check in decoded)
CheckedDomain(
name,
check['tld'],
status: switch (check['isRegistered']) {
true => CheckStatus.taken,
false => CheckStatus.available,
_ => CheckStatus.unknown,
},
)
];
}
14 changes: 14 additions & 0 deletions lib/misc.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import 'package:domine/models.dart';
import 'package:tint/tint.dart';

void domainTable(List<CheckedDomain> domains) {
table([
for (final domain in domains..sort())
'${(switch (domain.status) {
CheckStatus.available => '✔'.green(),
CheckStatus.taken => '⨯'.red(),
_ => '⁇'.blue(),
}).bold()} $domain'
]);
}

void table(List<String> input) {
const int columnCount = 5;
int remaining = columnCount - (input.length % columnCount), i;
Expand Down
2 changes: 2 additions & 0 deletions lib/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class CheckedDomain implements Comparable<CheckedDomain> {

const CheckedDomain(this.name, this.tld, {required this.status});

bool get available => status == CheckStatus.available;

@override
String toString() => '$name.$tld';

Expand Down
Loading

0 comments on commit 113cf03

Please sign in to comment.