Completed
Push — master ( cc24d0...5b3757 )
by Marcel
08:43
created

src/Traits/HandlesConversations.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace BotMan\BotMan\Traits;
4
5
use Closure;
6
use Illuminate\Support\Collection;
7
use Opis\Closure\SerializableClosure;
8
use BotMan\BotMan\Drivers\DriverManager;
9
use BotMan\BotMan\Interfaces\ShouldQueue;
10
use BotMan\BotMan\Messages\Outgoing\Question;
11
use BotMan\BotMan\Messages\Incoming\IncomingMessage;
12
use BotMan\BotMan\Messages\Conversations\Conversation;
13
14
trait HandlesConversations
15
{
16
    /**
17
     * @param \BotMan\BotMan\Messages\Conversations\Conversation $instance
18
     * @param null|string $recipient
19
     * @param null|string $driver
20
     */
21
    public function startConversation(Conversation $instance, $recipient = null, $driver = null)
22
    {
23
        if (! is_null($recipient) && ! is_null($driver)) {
24
            $this->message = new IncomingMessage('', $recipient, '');
0 ignored issues
show
The property message does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
25
            $this->driver = DriverManager::loadFromName($driver, $this->config);
26
        }
27
        $instance->setBot($this);
28
        $instance->run();
29
    }
30
31
    /**
32
     * @param \BotMan\BotMan\Messages\Conversations\Conversation $instance
33
     * @param array|Closure $next
34
     * @param string|Question $question
35
     * @param array $additionalParameters
36
     */
37
    public function storeConversation(Conversation $instance, $next, $question = null, $additionalParameters = [])
38
    {
39
        $conversation_cache_time = $instance->getConversationCacheTime();
40
41
        $this->cache->put($this->message->getConversationIdentifier(), [
42
            'conversation' => $instance,
43
            'question' => serialize($question),
44
            'additionalParameters' => serialize($additionalParameters),
45
            'next' => $this->prepareCallbacks($next),
46
            'time' => microtime(),
47
        ], $conversation_cache_time ?? $this->config['config']['conversation_cache_time'] ?? 30);
48
    }
49
50
    /**
51
     * Get a stored conversation array from the cache for a given message.
52
     *
53
     * @param null|IncomingMessage $message
54
     * @return array
55
     */
56
    public function getStoredConversation($message = null)
57
    {
58
        if (is_null($message)) {
59
            $message = $this->getMessage();
60
        }
61
62
        $conversation = $this->cache->get($message->getConversationIdentifier());
63
        if (is_null($conversation)) {
64
            $conversation = $this->cache->get($message->getOriginatedConversationIdentifier());
65
        }
66
67
        return $conversation;
68
    }
69
70
    /**
71
     * Touch and update the current conversation.
72
     *
73
     * @return void
74
     */
75
    public function touchCurrentConversation()
76
    {
77
        if (! is_null($this->currentConversationData)) {
78
            $touched = $this->currentConversationData;
79
            $touched['time'] = microtime();
80
81
            $this->cache->put($this->message->getConversationIdentifier(), $touched, $this->config['conversation_cache_time'] ?? 30);
82
        }
83
    }
84
85
    /**
86
     * Get the question that was asked in the currently stored conversation
87
     * for a given message.
88
     *
89
     * @param null|IncomingMessage $message
90
     * @return string|Question
91
     */
92
    public function getStoredConversationQuestion($message = null)
93
    {
94
        $conversation = $this->getStoredConversation($message);
95
96
        return unserialize($conversation['question']);
97
    }
98
99
    /**
100
     * Remove a stored conversation array from the cache for a given message.
101
     *
102
     * @param null|IncomingMessage $message
103
     */
104
    public function removeStoredConversation($message = null)
105
    {
106
        /*
107
         * Only remove it from the cache if it was not modified
108
         * after we loaded the data from the cache.
109
         */
110
        if ($this->getStoredConversation($message)['time'] == $this->currentConversationData['time']) {
111
            $this->cache->pull($this->message->getConversationIdentifier());
112
            $this->cache->pull($this->message->getOriginatedConversationIdentifier());
113
        }
114
    }
115
116
    /**
117
     * @param Closure $closure
118
     * @return string
119
     */
120
    public function serializeClosure(Closure $closure)
121
    {
122
        if ($this->getDriver()->serializesCallbacks() && ! $this->runsOnSocket) {
123
            return serialize(new SerializableClosure($closure, true));
124
        }
125
126
        return $closure;
127
    }
128
129
    /**
130
     * @param mixed $closure
131
     * @return string
132
     */
133
    protected function unserializeClosure($closure)
134
    {
135
        if ($this->getDriver()->serializesCallbacks() && ! $this->runsOnSocket) {
136
            return unserialize($closure);
137
        }
138
139
        return $closure;
140
    }
141
142
    /**
143
     * Prepare an array of pattern / callbacks before
144
     * caching them.
145
     *
146
     * @param array|Closure $callbacks
147
     * @return array
148
     */
149
    protected function prepareCallbacks($callbacks)
150
    {
151
        if (is_array($callbacks)) {
152
            foreach ($callbacks as &$callback) {
153
                $callback['callback'] = $this->serializeClosure($callback['callback']);
154
            }
155
        } else {
156
            $callbacks = $this->serializeClosure($callbacks);
157
        }
158
159
        return $callbacks;
160
    }
161
162
    /**
163
     * Look for active conversations and clear the payload
164
     * if a conversation is found.
165
     */
166
    public function loadActiveConversation()
167
    {
168
        $this->loadedConversation = false;
169
170
        Collection::make($this->getMessages())->reject(function (IncomingMessage $message) {
171
            return $message->isFromBot();
172
        })->filter(function (IncomingMessage $message) {
173
            return $this->cache->has($message->getConversationIdentifier()) || $this->cache->has($message->getOriginatedConversationIdentifier());
174
        })->each(function ($message) {
175
            $message = $this->middleware->applyMiddleware('received', $message);
176
            $message = $this->middleware->applyMiddleware('captured', $message);
177
178
            $convo = $this->getStoredConversation($message);
179
180
            // Should we skip the conversation?
181
            if ($convo['conversation']->skipsConversation($message) === true) {
182
                return;
183
            }
184
185
            // Or stop it entirely?
186
            if ($convo['conversation']->stopsConversation($message) === true) {
187
                $this->cache->pull($message->getConversationIdentifier());
188
                $this->cache->pull($message->getOriginatedConversationIdentifier());
189
190
                return;
191
            }
192
193
            $matchingMessages = $this->conversationManager->getMatchingMessages([$message], $this->middleware, $this->getConversationAnswer(), $this->getDriver(), false);
194
            foreach ($matchingMessages as $matchingMessage) {
195
                $command = $matchingMessage->getCommand();
196
                if ($command->shouldStopConversation()) {
197
                    $this->cache->pull($message->getConversationIdentifier());
198
                    $this->cache->pull($message->getOriginatedConversationIdentifier());
199
200
                    return;
201
                } elseif ($command->shouldSkipConversation()) {
202
                    return;
203
                }
204
            }
205
206
            // Ongoing conversation - let's find the callback.
207
            $next = false;
208
            $parameters = [];
209
            if (is_array($convo['next'])) {
210
                foreach ($convo['next'] as $callback) {
211
                    if ($this->matcher->isPatternValid($message, $this->getConversationAnswer(), $callback['pattern'])) {
212
                        $parameterNames = $this->compileParameterNames($callback['pattern']);
213
                        $matches = $this->matcher->getMatches();
214
215
                        if (count($parameterNames) === count($matches)) {
216
                            $parameters = array_combine($parameterNames, $matches);
217
                        } else {
218
                            $parameters = $matches;
219
                        }
220
                        $this->matches = $parameters;
221
                        $next = $this->unserializeClosure($callback['callback']);
222
                        break;
223
                    }
224
                }
225
            } else {
226
                $next = $this->unserializeClosure($convo['next']);
227
            }
228
229
            $this->message = $message;
230
            $this->currentConversationData = $convo;
231
232
            if (is_callable($next)) {
233
                $this->callConversation($next, $convo, $message, $parameters);
234
            }
235
        });
236
    }
237
238
    /**
239
     * @param callable $next
240
     * @param array $convo
241
     * @param IncomingMessage $message
242
     * @param array $parameters
243
     */
244
    protected function callConversation($next, $convo, IncomingMessage $message, array $parameters)
245
    {
246
        /** @var \BotMan\BotMan\Messages\Conversations\Conversation $conversation */
247
        $conversation = $convo['conversation'];
248
        if (! $conversation instanceof ShouldQueue) {
249
            $conversation->setBot($this);
250
        }
251
        /*
252
         * Validate askForImages, askForAudio, etc. calls
253
         */
254
        $additionalParameters = Collection::make(unserialize($convo['additionalParameters']));
255
        if ($additionalParameters->has('__pattern')) {
256
            if ($this->matcher->isPatternValid($message, $this->getConversationAnswer(), $additionalParameters->get('__pattern'))) {
257
                $getter = $additionalParameters->get('__getter');
258
                array_unshift($parameters, $this->getConversationAnswer()->getMessage()->$getter());
259
                $this->prepareConversationClosure($next, $conversation, $parameters);
260
            } else {
261
                if (is_null($additionalParameters->get('__repeat'))) {
262
                    $conversation->repeat();
263
                } else {
264
                    $next = unserialize($additionalParameters->get('__repeat'));
265
                    array_unshift($parameters, $this->getConversationAnswer());
266
                    $this->prepareConversationClosure($next, $conversation, $parameters);
267
                }
268
            }
269
        } else {
270
            array_unshift($parameters, $this->getConversationAnswer());
271
            $this->prepareConversationClosure($next, $conversation, $parameters);
272
        }
273
274
        // Mark conversation as loaded to avoid triggering the fallback method
275
        $this->loadedConversation = true;
276
        $this->removeStoredConversation();
277
    }
278
279
    /**
280
     * @param Closure $next
281
     * @param Conversation $conversation
282
     * @param array $parameters
283
     */
284
    protected function prepareConversationClosure($next, Conversation $conversation, array $parameters)
285
    {
286
        if ($next instanceof SerializableClosure) {
287
            $next = $next->getClosure()->bindTo($conversation, $conversation);
288
        }
289
290
        $parameters[] = $conversation;
291
        call_user_func_array($next, $parameters);
292
    }
293
}
294