Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

workflow triggers #4

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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
15 changes: 15 additions & 0 deletions example/workflows/join.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: 1

name: Join test

on:
- workflow_dispatch
- join
- text:
match: hello
Comment on lines +6 to +9
Copy link
Member

@krdlab krdlab Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

基本的に項目の重複はないと思いますので,リストよりはハッシュ構造の方が良さそうです.
(text 等は複数発生するかもしれませんが,それは text フィールド以下の属性で表現するイメージです)


steps:
- name: send text
action: daab:message:text
with:
text: はじめまして
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class Commands {
session.selecting = true;
res.send({
question: 'ワークフローを選択してください。',
options: workflows.getNames(),
options: workflows.getSelectableNames(),
});
}
}
Expand Down
157 changes: 153 additions & 4 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import path from 'path';
import * as uuid from 'uuid';
import yaml from 'js-yaml';
import { Action, CustomAction, MessageAction } from './actions';
import { parseTrigger, isTriggerFired } from './triggers';
import {
DirectUser,
DirectTalk,
Expand All @@ -19,6 +20,11 @@ import {
TaskWithResponse,
TextMessage,
YesNoWithResponse,
NoteCreated,
NoteUpdated,
NoteDeleted,
JoinMessage,
LeaveMessage,
} from './daab';
import {
DefaultAction,
Expand All @@ -28,6 +34,9 @@ import {
DefaultActionWith,
isCustomAction,
getCustomActionName,
WorkflowEvent,
WorkflowEventType,
WorkflowEventWith,
} from './workflow';
import { Repository } from './repository';

Expand All @@ -47,7 +56,7 @@ export class Workflows {

const docs = new Map<string, Workflow>();
filenames.forEach((fn) => {
const w = yaml.load(fs.readFileSync(fn, 'utf8'));
const w = this.parse(yaml.load(fs.readFileSync(fn, 'utf8')));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このタイミングで parse (の中でやっている処理を) するのではなく,validate 後の方が良さそうに思いました.
(Workflow 型であることは type guard 後に決定するため)

if (this.validate(w)) {
docs.set(w.name, w);
} else {
Expand All @@ -57,14 +66,39 @@ export class Workflows {
return new Workflows(docs, repository);
}

static validate(obj: any): obj is Workflow {
return typeof obj === 'object' && obj.version && obj.name && Array.isArray(obj.steps);
static parse(w: any): Workflow {
if (w) {
w.on = parseTrigger(w.on);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate を type guard 関数へ戻すことを前提に,60 行目付近で以下のように呼び出す想定です.

      if (this.validate(w)) {
        w.on = parseTrigger(w.on); // ← ここでは on がエラーにならない
        docs.set(w.name, w);
      } else {

(※ parse 関数は削除します)

}
return w;
}

static validate(obj: Workflow): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらは type guard 関数に戻したいです.

Suggested change
static validate(obj: Workflow): boolean {
static validate(obj: any): obj is Workflow

return (
typeof obj === 'object' &&
typeof obj.version === 'number' &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(元々 'string' を想定していましたが,しばらくは 'number' でも困らないのでここままで OK です)

!!obj.version &&
typeof obj.name === 'string' &&
!!obj.name &&
Array.isArray(obj.steps) &&
typeof obj.on === 'object'
);
}

getNames(): string[] {
return Array.from(this.docs.keys()).sort();
}

getSelectableNames(): string[] {
return this.filterByEvent(WorkflowEvent.WorkflowDispatch).map((workflow) => workflow.name);
}

filterByEvent(type: WorkflowEventType, e?: WorkflowEventWith): Workflow[] {
return this.getNames()
.map((name) => this.findByName(name)!)
.filter((workflow) => isTriggerFired(type, workflow.on[type], e));
}

findByName(name: string): Workflow | undefined {
return this.docs.get(name);
}
Expand All @@ -76,6 +110,16 @@ export class Workflows {
}
return undefined;
}

createWorkflowContextByEvent(
type: WorkflowEventType,
e?: WorkflowEventWith
): WorkflowContext | undefined {
const workflow = this.filterByEvent(type, e);
if (workflow.length) {
return WorkflowContext.create(workflow[0], this.repository);
}
}
}

// * NOTE: Hubot の Listener 処理が Promise に対応していないため,daab-session を使わずに別途設けることになった
Expand Down Expand Up @@ -148,6 +192,7 @@ export class WorkflowContext {
private valid = true;
private stepIndex = 0;
private data: WorkflowStepData = {};
private readonly firstStep = { id: 'on' } as WorkflowStep;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firstStep を内部生成するのではなく,トリガーで発生した情報を this.data に保存するのみにした方が良さそうに思います.
(resetByEvent 側の調整も必要)

private readonly actors = new Set<string>();

private constructor(
Expand Down Expand Up @@ -205,7 +250,14 @@ export class WorkflowContext {
this.data = {};
}

private resetByEvent(type: WorkflowEventType) {
this.stepIndex = -1;
this.data = {};
this.firstStep.action = `daab:message:${type.split('_')[0]}`;
}

private get currentStep() {
if (this.stepIndex === -1) return this.firstStep;
return this.workflow.steps[this.stepIndex];
}
private get isLastStep() {
Expand All @@ -231,7 +283,14 @@ export class WorkflowContext {

private getUserId(res: Response<any>, step: WorkflowStep) {
const args = step.with as { to?: string }; // ! FIXME
return this.findUserId(res, args.to)?.id ?? res.message.user.id;
// FIXME
const obj = (res.robot as any).direct.api.dataStore.me.id;
const botId = `_${obj.high}_${obj.low}`;
let userId = res.message.user.id;
if (userId == botId) {
userId = res.message.roomUsers.find((user: any) => user.id !== botId)?.id;
}
return this.findUserId(res, args.to)?.id ?? userId;
}

private async findOrCreateUserContext(userId: string) {
Expand Down Expand Up @@ -312,6 +371,11 @@ export class WorkflowContext {
}
}

async triggerWorkflow(type: WorkflowEventType) {
this.resetByEvent(type);
this.activate();
}

private async exitWorkflow() {
this.reset();
this.deactivate();
Expand All @@ -330,6 +394,10 @@ export class WorkflowContext {
await this.repository.destroy(this);
}

async cancelWorkflow() {
return this.exitWorkflow();
}

async handleSelect(res: ResponseWithJson<SelectWithResponse>) {
const current = this.currentStep;
if (current.action != DefaultAction.Select) {
Expand Down Expand Up @@ -396,4 +464,85 @@ export class WorkflowContext {

await this.runNextAction(res);
}

async handleNoteCreated(res: ResponseWithJson<NoteCreated>) {
const current = this.currentStep;
if (current.action != DefaultAction.Note) {
return;
}

if (current.id) {
this.data[current.id] = {
responder: res.message.user,
...res.json,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res.json の中身を制限なく展開してしまうことは避けたいです.response.note の方のみを使うようにできないでしょうか?

response: { note: res.json },
};
}

await this.runNextAction(res);
}

async handleNoteUpdated(res: ResponseWithJson<NoteUpdated>) {
const current = this.currentStep;
if (current.action != DefaultAction.Note) {
return;
}

if (current.id) {
this.data[current.id] = {
responder: res.message.user,
...res.json,
response: { note: res.json },
};
}

await this.runNextAction(res);
}

async handleNoteDeleted(res: ResponseWithJson<NoteDeleted>) {
const current = this.currentStep;
if (current.action != DefaultAction.Note) {
return;
}

if (current.id) {
this.data[current.id] = {
responder: res.message.user,
...res.json,
response: { note: res.json },
};
}

await this.runNextAction(res);
}

async handleJoin(res: Response<JoinMessage>) {
const current = this.currentStep;
if (current.action != 'daab:message:join') {
return;
}

if (current.id) {
this.data[current.id] = {
responder: res.message.user,
};
}

await this.runNextAction(res);
}

async handleLeave(res: Response<LeaveMessage>) {
const current = this.currentStep;
if (current.action != 'daab:message:leave') {
return;
}

if (current.id) {
this.data[current.id] = {
responder: res.message.user,
};
}

await this.runNextAction(res);
}
}
Loading