Skip to content

Commit

Permalink
feat: remove fragments after completion (#85)
Browse files Browse the repository at this point in the history
store cmd data in fragments for later completion (required for handlers with required data)
add completion helper to resolve noun circular deps
add helper for numeric cmd data
  • Loading branch information
ssube committed Dec 29, 2018
1 parent 605e80e commit c96e1e2
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 27 deletions.
33 changes: 30 additions & 3 deletions src/controller/CompletionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BaseController } from 'src/controller/BaseController';
import { Controller, ControllerOptions } from 'src/controller/Controller';
import { Command, CommandVerb } from 'src/entity/Command';
import { Fragment } from 'src/entity/Fragment';
import { InvalidArgumentError } from 'src/error/InvalidArgumentError';
import { Parser } from 'src/parser/Parser';
import { mapToDict } from 'src/utils/Map';

Expand Down Expand Up @@ -82,17 +83,43 @@ export class CompletionController extends BaseController<CompletionControllerDat
return this.reply(cmd.context, 'fragment not found');
}

this.logger.debug({ fragment }, 'attempting to complete fragment');
this.logger.debug({ fragment, parserId: fragment.parserId }, 'attempting to complete fragment');

try {
this.logger.debug({ parserId: fragment.parserId }, 'getting parser for fragment');
const parser = this.services.getService<Parser>({ id: fragment.parserId });
const value = cmd.get('next');
const commands = await parser.complete(cmd.context, fragment, value);

// the commands have been completed (or additional completions issued), so even if they fail,
// the previous fragment should be cleaned up. If parsing fails, the fragment should not be
// cleaned up.
await this.fragmentRepository.delete(fragment.id);
await this.bot.executeCommand(...commands);
} catch (err) {
this.logger.error(err, 'error completing fragment');
await this.reply(cmd.context, 'error completing fragment');
return this.reply(cmd.context, 'error completing fragment');
}
}
}

export function createCompletion(cmd: Command, key: string, msg: string): Command {
if (!cmd.context.parser) {
throw new InvalidArgumentError('command has no parser to prompt for completion');
}

const existingData = mapToDict(cmd.data);
return new Command({
context: cmd.context,
data: {
...existingData,
key: [key],
msg: [msg],
noun: [cmd.noun],
parser: [cmd.context.parser.id],
verb: [cmd.verb],
},
labels: {},
noun: NOUN_FRAGMENT,
verb: CommandVerb.Create,
});
}
8 changes: 8 additions & 0 deletions src/controller/SessionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UserRepository } from 'src/entity/auth/UserRepository';
import { Command, CommandVerb } from 'src/entity/Command';
import { Clock } from 'src/utils/Clock';

import { createCompletion } from './CompletionController';

export const NOUN_GRANT = 'grant';
export const NOUN_JOIN = 'join';
export const NOUN_SESSION = 'session';
Expand Down Expand Up @@ -141,6 +143,12 @@ export class SessionController extends BaseController<SessionControllerData> imp
return this.reply(cmd.context, 'must be logged in');
}

if (cmd.getHeadOrDefault('confirm', 'no') !== 'yes') {
const completion = createCompletion(cmd, 'confirm', `please confirm deleting all tokens for ${cmd.context.user.name}`);
await this.bot.executeCommand(completion);
return;
}

await this.tokenRepository.delete({
subject: cmd.context.user.id,
});
Expand Down
31 changes: 7 additions & 24 deletions src/controller/TokenController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Inject } from 'noicejs';
import { Connection, Equal, Repository } from 'typeorm';
import { Connection, Equal, LessThan, Repository } from 'typeorm';

import { BaseController } from 'src/controller/BaseController';
import { NOUN_FRAGMENT } from 'src/controller/CompletionController';
import { createCompletion } from 'src/controller/CompletionController';
import { Controller, ControllerData, ControllerOptions } from 'src/controller/Controller';
import { Token } from 'src/entity/auth/Token';
import { Command, CommandVerb } from 'src/entity/Command';
import { InvalidArgumentError } from 'src/error/InvalidArgumentError';
import { Clock } from 'src/utils/Clock';

const MSG_SESSION_REQUIRED = 'must be logged in';
Expand Down Expand Up @@ -90,11 +89,15 @@ export class TokenController extends BaseController<TokenControllerData> impleme
return this.reply(cmd.context, MSG_SESSION_REQUIRED);
}

const before = cmd.getHeadOrNumber('before', this.clock.getSeconds());
if (cmd.getHeadOrDefault('confirm', 'no') !== 'yes') {
return this.requestCompletion(cmd, 'confirm', `please confirm deleting all tokens for ${cmd.context.user.id}`);
const completion = createCompletion(cmd, 'confirm', `please confirm deleting tokens for ${cmd.context.user.name} from before ${before}`);
await this.bot.executeCommand(completion);
return;
}

const results = await this.tokenRepository.delete({
createdAt: LessThan(before),
subject: Equal(cmd.context.user.id),
});

Expand Down Expand Up @@ -138,24 +141,4 @@ export class TokenController extends BaseController<TokenControllerData> impleme

return this.reply(cmd.context, JSON.stringify(tokens));
}

protected async requestCompletion(cmd: Command, key: string, msg: string): Promise<void> {
if (!cmd.context.parser) {
throw new InvalidArgumentError('command has no parser to prompt for completion');
}

await this.bot.executeCommand(new Command({
context: cmd.context,
data: {
key: [key],
msg: [msg],
noun: [cmd.noun],
parser: [cmd.context.parser.id],
verb: [cmd.verb],
},
labels: {},
noun: NOUN_FRAGMENT,
verb: CommandVerb.Create,
}));
}
}
9 changes: 9 additions & 0 deletions src/entity/base/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,14 @@ export abstract class BaseCommand extends DataEntity<Array<string>> {
return getHeadOrDefault(this.data, key, defaultValue);
}

public getHeadOrNumber(key: string, defaultValue: number): number {
const value = Number(this.getHead(key));
if (isNaN(value)) {
return defaultValue;
} else {
return value;
}
}

public abstract toJSON(): object;
}

0 comments on commit c96e1e2

Please sign in to comment.