1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace FondBot\Conversation; |
||||
6 | |||||
7 | use Closure; |
||||
8 | use FondBot\Channels\Chat; |
||||
9 | use FondBot\Channels\User; |
||||
10 | use FondBot\Channels\Channel; |
||||
11 | use FondBot\Contracts\Activator; |
||||
12 | use Illuminate\Cache\Repository; |
||||
13 | use FondBot\Contracts\Conversable; |
||||
14 | use FondBot\Events\MessageReceived; |
||||
15 | use Illuminate\Contracts\Foundation\Application; |
||||
16 | |||||
17 | class ConversationManager |
||||
18 | { |
||||
19 | private $intents = []; |
||||
20 | private $fallbackIntent; |
||||
21 | |||||
22 | private $application; |
||||
23 | private $cache; |
||||
24 | |||||
25 | private $transitioned = false; |
||||
26 | |||||
27 | private $messageReceived; |
||||
28 | 82 | ||||
29 | public function __construct(Application $application, Repository $cache) |
||||
30 | 82 | { |
|||
31 | 82 | $this->application = $application; |
|||
32 | 82 | $this->cache = $cache; |
|||
33 | } |
||||
34 | |||||
35 | 2 | /** |
|||
36 | * Register intent. |
||||
37 | 2 | * |
|||
38 | 2 | * @param string $class |
|||
39 | */ |
||||
40 | public function registerIntent(string $class): void |
||||
41 | 82 | { |
|||
42 | $this->intents[] = $class; |
||||
43 | 82 | } |
|||
44 | 82 | ||||
45 | /** |
||||
46 | * Register fallback intent. |
||||
47 | 1 | * |
|||
48 | * @param string $class |
||||
49 | 1 | */ |
|||
50 | public function registerFallbackIntent(string $class): void |
||||
51 | { |
||||
52 | $this->fallbackIntent = $class; |
||||
53 | 1 | } |
|||
54 | |||||
55 | 1 | /** |
|||
56 | * Get all registered intents. |
||||
57 | 1 | * |
|||
58 | * @return array |
||||
59 | 1 | */ |
|||
60 | 1 | public function getIntents(): array |
|||
61 | 1 | { |
|||
62 | return $this->intents; |
||||
63 | } |
||||
64 | |||||
65 | /** |
||||
66 | * Match intent by received message. |
||||
67 | 1 | * |
|||
68 | * @param MessageReceived $messageReceived |
||||
69 | * |
||||
70 | * @return Intent|null |
||||
71 | 1 | */ |
|||
72 | public function matchIntent(MessageReceived $messageReceived): ?Intent |
||||
73 | 1 | { |
|||
74 | 1 | foreach ($this->intents as $intent) { |
|||
75 | 1 | /** @var Intent $intent */ |
|||
76 | $intent = resolve($intent); |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
77 | |||||
78 | foreach ($intent->activators() as $activator) { |
||||
79 | if (!$intent->passesAuthorization($messageReceived)) { |
||||
80 | continue; |
||||
81 | 1 | } |
|||
82 | |||||
83 | 1 | if ($activator instanceof Closure && value($activator($messageReceived)) === true) { |
|||
84 | 1 | return $intent; |
|||
85 | } |
||||
86 | |||||
87 | 1 | if ($activator instanceof Activator && $activator->matches($messageReceived)) { |
|||
88 | 1 | return $intent; |
|||
89 | } |
||||
90 | } |
||||
91 | } |
||||
92 | 1 | ||||
93 | // Otherwise, return fallback intent |
||||
94 | 1 | return resolve($this->fallbackIntent); |
|||
0 ignored issues
–
show
|
|||||
95 | } |
||||
96 | |||||
97 | /** |
||||
98 | 1 | * Resolve conversation context. |
|||
99 | * |
||||
100 | 1 | * @param Channel $channel |
|||
101 | 1 | * @param Chat $chat |
|||
102 | 1 | * @param User $user |
|||
103 | * |
||||
104 | 1 | * @return Context |
|||
105 | */ |
||||
106 | public function resolveContext(Channel $channel, Chat $chat, User $user): Context |
||||
107 | { |
||||
108 | $value = $this->cache->get($this->getCacheKeyForContext($channel, $chat, $user), [ |
||||
109 | 'chat' => $chat, |
||||
110 | 'user' => $user, |
||||
111 | 'intent' => null, |
||||
112 | 'interaction' => null, |
||||
113 | 'items' => [], |
||||
114 | ]); |
||||
115 | 82 | ||||
116 | $context = new Context($channel, $chat, $user, $value['items'] ?? []); |
||||
117 | 82 | ||||
118 | 82 | if (isset($value['intent'])) { |
|||
119 | $context->setIntent(resolve($value['intent'])); |
||||
0 ignored issues
–
show
It seems like
resolve($value['intent']) can also be of type Illuminate\Foundation\Application ; however, parameter $intent of FondBot\Conversation\Context::setIntent() does only seem to accept FondBot\Conversation\Intent , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
120 | } |
||||
121 | 15 | ||||
122 | if (isset($value['interaction'])) { |
||||
123 | $context->setInteraction(resolve($value['interaction'])); |
||||
0 ignored issues
–
show
It seems like
resolve($value['interaction']) can also be of type Illuminate\Foundation\Application ; however, parameter $interaction of FondBot\Conversation\Context::setInteraction() does only seem to accept null|FondBot\Conversation\Interaction , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
124 | } |
||||
125 | |||||
126 | // Bind resolved instance to the container |
||||
127 | $this->application->instance('fondbot.conversation.context', $context); |
||||
128 | |||||
129 | return $context; |
||||
130 | } |
||||
131 | |||||
132 | /** |
||||
133 | * Save context. |
||||
134 | * |
||||
135 | * @param Context $context |
||||
136 | */ |
||||
137 | public function saveContext(Context $context): void |
||||
138 | { |
||||
139 | $this->cache->forever( |
||||
140 | $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser()), |
||||
141 | $context->toArray() |
||||
142 | ); |
||||
143 | } |
||||
144 | |||||
145 | /** |
||||
146 | * Flush context. |
||||
147 | * |
||||
148 | * @param Context $context |
||||
149 | */ |
||||
150 | public function flushContext(Context $context): void |
||||
151 | { |
||||
152 | $this->cache->forget( |
||||
153 | $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser()) |
||||
154 | 82 | ); |
|||
155 | } |
||||
156 | 82 | ||||
157 | /** |
||||
158 | 82 | * Get current context. |
|||
159 | 82 | * |
|||
160 | * @return Context|null |
||||
161 | */ |
||||
162 | public function getContext(): ?Context |
||||
163 | { |
||||
164 | if (!$this->application->has('fondbot.conversation.context')) { |
||||
165 | return null; |
||||
166 | } |
||||
167 | |||||
168 | return $this->application->get('fondbot.conversation.context'); |
||||
169 | } |
||||
170 | |||||
171 | /** |
||||
172 | * Define received message. |
||||
173 | 2 | * |
|||
174 | * @param MessageReceived $messageReceived |
||||
175 | 2 | */ |
|||
176 | public function setReceivedMessage(MessageReceived $messageReceived): void |
||||
177 | { |
||||
178 | $this->messageReceived = $messageReceived; |
||||
179 | } |
||||
180 | |||||
181 | /** |
||||
182 | * Mark conversation as transitioned. |
||||
183 | */ |
||||
184 | public function markAsTransitioned(): void |
||||
185 | { |
||||
186 | $this->transitioned = true; |
||||
187 | } |
||||
188 | |||||
189 | /** |
||||
190 | * Determine if conversation has been transitioned. |
||||
191 | * |
||||
192 | * @return bool |
||||
193 | */ |
||||
194 | public function transitioned(): bool |
||||
195 | { |
||||
196 | return $this->transitioned; |
||||
197 | } |
||||
198 | |||||
199 | /** |
||||
200 | * Start conversation. |
||||
201 | * |
||||
202 | * @param Conversable $conversable |
||||
203 | */ |
||||
204 | public function converse(Conversable $conversable): void |
||||
205 | { |
||||
206 | context()->incrementAttempts(); |
||||
207 | |||||
208 | if ($conversable instanceof Intent) { |
||||
209 | context()->setIntent($conversable)->setInteraction(null); |
||||
210 | } |
||||
211 | |||||
212 | $conversable->handle($this->messageReceived); |
||||
213 | } |
||||
214 | |||||
215 | /** |
||||
216 | * Restart interaction. |
||||
217 | * |
||||
218 | * @param Interaction $interaction |
||||
219 | */ |
||||
220 | public function restartInteraction(Interaction $interaction): void |
||||
221 | { |
||||
222 | context()->setInteraction(null); |
||||
223 | |||||
224 | $this->converse($interaction); |
||||
225 | |||||
226 | $this->markAsTransitioned(); |
||||
227 | } |
||||
228 | |||||
229 | public function __destruct() |
||||
230 | { |
||||
231 | $context = $this->getContext(); |
||||
232 | |||||
233 | if ($context === null) { |
||||
234 | return; |
||||
235 | } |
||||
236 | |||||
237 | // Close session if conversation has not been transitioned |
||||
238 | if (!$this->transitioned()) { |
||||
239 | $this->flushContext($context); |
||||
240 | } |
||||
241 | |||||
242 | // Save context if exists |
||||
243 | if ($this->transitioned() && $context = context()) { |
||||
244 | $this->saveContext($context); |
||||
245 | } |
||||
246 | } |
||||
247 | |||||
248 | private function getCacheKeyForContext(Channel $channel, Chat $chat, User $user): string |
||||
249 | { |
||||
250 | return implode('.', ['context', $channel->getName(), $chat->getId(), $user->getId()]); |
||||
251 | } |
||||
252 | } |
||||
253 |