├── .gitignore
├── logo.png
├── examples
├── demo.gif
├── images.php
├── chat.php
└── multi.php
├── src
├── Chat
│ ├── Tone.php
│ ├── MessageType.php
│ ├── Prompt.php
│ ├── Message.php
│ └── Conversation.php
├── BingAI.php
├── Tools.php
└── Images
│ └── ImageCreator.php
├── composer.json
├── LICENSE
├── README.md
└── composer.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | .idea/
3 | .DS_Store
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximerenou/php-bing-ai/HEAD/logo.png
--------------------------------------------------------------------------------
/examples/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maximerenou/php-bing-ai/HEAD/examples/demo.gif
--------------------------------------------------------------------------------
/src/Chat/Tone.php:
--------------------------------------------------------------------------------
1 | =7.1",
18 | "ratchet/pawl": "^0.4.1",
19 | "ext-curl": "*"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Chat/MessageType.php:
--------------------------------------------------------------------------------
1 | text = $text;
17 | }
18 |
19 | public function withPreferences($locale = 'en-US', $market = 'en-US', $region = 'US')
20 | {
21 | $this->locale = $locale;
22 | $this->market = $market;
23 | $this->region = $region;
24 | return $this;
25 | }
26 |
27 | public function withoutCache()
28 | {
29 | $this->cache = false;
30 | return $this;
31 | }
32 |
33 | public function withImage($image, $is_rawdata = false)
34 | {
35 | if (! $is_rawdata) {
36 | $image = file_get_contents($image);
37 | }
38 |
39 | $this->image = $image;
40 | return $this;
41 | }
42 | }
--------------------------------------------------------------------------------
/examples/images.php:
--------------------------------------------------------------------------------
1 | getImageCreator()->getRemainingBoosts();
16 |
17 | echo "You have $boosts remaining boosts." . PHP_EOL;
18 | echo 'Type "q" to quit' . PHP_EOL;
19 |
20 | echo PHP_EOL . "> ";
21 | $text = rtrim(fgets(STDIN));
22 |
23 | if ($text == 'q')
24 | exit(0);
25 |
26 | $creator = $ai->createImages($text);
27 |
28 | echo 'Generating...' . PHP_EOL;
29 |
30 | $creator->wait();
31 |
32 | if (! $creator->hasFailed()) {
33 | $images = $creator->getImages();
34 |
35 | foreach ($images as $image) {
36 | echo "- $image" . PHP_EOL;
37 | }
38 | }
39 | else {
40 | echo 'Generation failed' . PHP_EOL;
41 | exit(1);
42 | }
43 |
44 | exit(0);
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Maxime Renou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/BingAI.php:
--------------------------------------------------------------------------------
1 | cookie = $cookie;
15 | }
16 |
17 | public function createChatConversation($data = null)
18 | {
19 | return new Conversation($this->cookie, $data, 0);
20 | }
21 |
22 | public function resumeChatConversation($data = null, $invocations = 1)
23 | {
24 | return new Conversation($this->cookie, $data, $invocations);
25 | }
26 |
27 | public function getImageCreator()
28 | {
29 | return new ImageCreator($this->cookie);
30 | }
31 |
32 | public function createImages($prompt)
33 | {
34 | return $this->getImageCreator()->create($prompt);
35 | }
36 |
37 | public function checkCookie()
38 | {
39 | $html = Tools::request('https://www.bing.com', [
40 | 'cookie: _U=' . $this->cookie,
41 | 'method: GET',
42 | 'accept: text/html'
43 | ]);
44 |
45 | return strpos($html, '') === false;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Tools.php:
--------------------------------------------------------------------------------
1 | createChatConversation();
18 |
19 | echo 'Type "q" to quit' . PHP_EOL;
20 |
21 | while (true) {
22 | echo PHP_EOL . "> ";
23 | $text = rtrim(fgets(STDIN));
24 |
25 | if ($text == 'q')
26 | break;
27 |
28 | if (strpos($text, '$image') !== false) {
29 | $text = str_replace('$image', '', $text);
30 | $prompt = new \MaximeRenou\BingAI\Chat\Prompt($text);
31 | echo PHP_EOL . "Image path: ";
32 | $image = rtrim(fgets(STDIN));
33 | $prompt->withImage($image);
34 | } else {
35 | $prompt = new \MaximeRenou\BingAI\Chat\Prompt($text);
36 | }
37 |
38 | $padding = 0;
39 |
40 | list($text, $cards) = $conversation->ask($prompt, function ($text, $cards) use (&$padding) {
41 | // Erase last line
42 | echo str_repeat(chr(8), $padding);
43 |
44 | // Print partial answer
45 | echo "- $text";
46 | $padding = mb_strlen($text) + 2;
47 | });
48 |
49 | // Erase last line
50 | echo str_repeat(chr(8), $padding);
51 |
52 | // Print final answer
53 | echo "- $text" . PHP_EOL;
54 |
55 | if ($conversation->kicked()) {
56 | echo "[Conversation ended]" . PHP_EOL;
57 | break;
58 | }
59 |
60 | $remaining = $conversation->getRemainingMessages();
61 |
62 | if ($remaining != 0) {
63 | echo "[$remaining remaining messages]" . PHP_EOL;
64 | } else {
65 | echo "[Limit reached]" . PHP_EOL;
66 | break;
67 | }
68 | }
69 |
70 | exit(0);
71 |
--------------------------------------------------------------------------------
/examples/multi.php:
--------------------------------------------------------------------------------
1 | createChatConversation()
16 | ->withTone(\MaximeRenou\BingAI\Chat\Tone::Creative);
17 |
18 | echo 'Type "q" to quit' . PHP_EOL;
19 |
20 | while (true) {
21 | echo PHP_EOL . "> ";
22 | $text = rtrim(fgets(STDIN));
23 |
24 | if ($text == 'q')
25 | break;
26 |
27 | $prompt = new \MaximeRenou\BingAI\Chat\Prompt($text);
28 | $padding = 0;
29 |
30 | list($text, $cards) = $conversation->ask($prompt, function ($text, $cards) use (&$padding) {
31 | // Erase last line
32 | echo str_repeat(chr(8), $padding);
33 |
34 | $text = trim($text);
35 |
36 | // Print partial answer
37 | echo "- $text";
38 | $padding = mb_strlen($text) + 2;
39 | });
40 |
41 | // Erase last line
42 | echo str_repeat(chr(8), $padding);
43 |
44 | // Print final answer
45 | echo "- $text" . PHP_EOL;
46 |
47 | // Generative cards
48 | foreach ($cards as $card) {
49 | if ($card->type == \MaximeRenou\BingAI\Chat\MessageType::GenerateQuery && $card->data['contentType'] == 'IMAGE') {
50 | $loader = "Generating: {$card->text}...";
51 | echo $loader;
52 |
53 | // Create the image
54 | $creator = $ai->createImages($card->text);
55 | $creator->wait();
56 |
57 | echo str_repeat(chr(8), strlen($loader));
58 |
59 | if ($creator->hasFailed()) {
60 | echo "[Image generation failed]" . PHP_EOL;
61 | }
62 | else {
63 | foreach ($creator->getImages() as $image) {
64 | echo "* $image" . PHP_EOL;
65 | }
66 |
67 | $remaining = $creator->getRemainingBoosts();
68 | echo "[$remaining remaining boosts]" . PHP_EOL;
69 | }
70 | }
71 | }
72 |
73 | if ($conversation->ended()) {
74 | echo "[Conversation ended]" . PHP_EOL;
75 | break;
76 | }
77 | }
78 |
79 | exit(0);
80 |
--------------------------------------------------------------------------------
/src/Chat/Message.php:
--------------------------------------------------------------------------------
1 | text = $prompt->text;
11 | $message->type = MessageType::Prompt;
12 | return $message;
13 | }
14 |
15 | public static function fromData($data)
16 | {
17 | $message = new self($data);
18 | $message->id = $data['messageId'];
19 |
20 | switch ($data['messageType'] ?? '') {
21 | case 'InternalSearchQuery':
22 | $message->type = MessageType::InternalSearchQuery;
23 | break;
24 | case 'InternalSearchResult':
25 | $message->type = MessageType::SearchResult;
26 | break;
27 | case 'InternalLoaderMessage':
28 | $message->type = MessageType::Loader;
29 | break;
30 | case 'SemanticSerp':
31 | $message->type = MessageType::SemanticSerp;
32 | break;
33 | case 'Disengaged':
34 | $message->type = MessageType::Disengaged;
35 | break;
36 | case 'AdsQuery':
37 | $message->type = MessageType::AdsQuery;
38 | break;
39 | case 'ActionRequest':
40 | $message->type = MessageType::ActionRequest;
41 | break;
42 | case 'RenderCardRequest':
43 | $message->type = MessageType::RenderRequest;
44 | break;
45 | case 'SearchQuery':
46 | $message->type = MessageType::SearchQuery;
47 | break;
48 | case 'GenerateContentQuery':
49 | $message->type = MessageType::GenerateQuery;
50 | break;
51 | break;
52 | case 'Context':
53 | $message->type = MessageType::Context;
54 | break;
55 | break;
56 | case 'Progress':
57 | $message->type = MessageType::Progress;
58 | break;
59 | default:
60 | $message->type = $data['author'] == 'user' ? MessageType::Prompt : MessageType::Answer;
61 | }
62 |
63 | if (! empty($data['text']))
64 | $message->text = $data['text'];
65 | elseif (! empty($data['hiddenText']))
66 | $message->text = $data['hiddenText'];
67 |
68 | return $message;
69 | }
70 |
71 | public $id;
72 | public $text;
73 | public $type;
74 | public $data;
75 |
76 | public function __construct($data = null)
77 | {
78 | $this->data = $data;
79 | }
80 |
81 | public function toText()
82 | {
83 | if (! empty($this->text)) {
84 | return preg_replace('/\[\^[0-9]+\^]/', '', $this->text);
85 | }
86 |
87 | return false;
88 | }
89 |
90 | public function toArray()
91 | {
92 | return [
93 | 'id' => $this->id,
94 | 'text' => $this->text,
95 | 'formatted_text' => $this->toText(),
96 | 'type' => $this->type,
97 | 'data' => $this->data
98 | ];
99 | }
100 |
101 | public function jsonSerialize(): mixed
102 | {
103 | return $this->toArray();
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Images/ImageCreator.php:
--------------------------------------------------------------------------------
1 | cookie = $cookie;
19 | }
20 |
21 | public function resume($generation_id, $prompt)
22 | {
23 | $this->generation_id = $generation_id;
24 | $this->prompt = $prompt;
25 | $this->generating = true;
26 |
27 | return $this;
28 | }
29 |
30 | public function create($prompt)
31 | {
32 | $this->prompt = $prompt;
33 |
34 | $prompt_encoded = urlencode($prompt);
35 | $url = "https://www.bing.com/images/create?q=$prompt_encoded&rt=4&FORM=GENCRE";
36 | $headers = [
37 | 'cookie: _U=' . $this->cookie,
38 | 'method: POST',
39 | 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
40 | "accept-language: en;q=0.9",
41 | 'content-type: application/x-www-form-urlencoded',
42 | 'referer: https://www.bing.com/images/create?FORM=GENILP'
43 | ];
44 |
45 | list($data, $request, $url) = Tools::request($url, $headers, "q=$prompt_encoded&qs=ds", true);
46 |
47 | $query = parse_url($url, PHP_URL_QUERY);
48 | $params = [];
49 |
50 | Tools::debug("Query $query");
51 |
52 | if (! empty($query)) {
53 | parse_str($query, $params);
54 |
55 | if (isset($params['id'])) {
56 | Tools::debug("ID {$params['id']}");
57 |
58 | $this->generation_id = $params['id'];
59 | $this->generating = true;
60 |
61 | return $this;
62 | }
63 | }
64 |
65 | $this->failed = true;
66 |
67 | return $this;
68 | }
69 |
70 | public function hasFailed()
71 | {
72 | return $this->failed;
73 | }
74 |
75 | public function isGenerating()
76 | {
77 | if ($this->hasFailed())
78 | return false;
79 |
80 | $prompt_encoded = urlencode($this->prompt);
81 | $url = "https://www.bing.com/images/create/async/results/{$this->generation_id}?q={$prompt_encoded}";
82 |
83 | $data = Tools::request($url, [
84 | 'cookie: _U=' . $this->cookie,
85 | 'method: GET',
86 | 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
87 | "accept-language: en;q=0.9",
88 | 'content-type: application/x-www-form-urlencoded',
89 | 'referer: https://www.bing.com/images/create?FORM=GENILP'
90 | ]);
91 |
92 | if (! is_string($data))
93 | return $this->generating;
94 |
95 | if (preg_match_all('/(https:\/\/th.bing\.com\/th\/id\/[A-Za-z0-9._-]+)\?/', $data, $matches)) {
96 | $this->images = $matches[1];
97 | $this->generating = false;
98 | Tools::debug("Images: " . implode(", ", $this->images));
99 | }
100 | elseif ($data = json_decode($data, true)) {
101 | if (! empty($data["errorMessage"])) {
102 | $this->failed = true;
103 | Tools::debug("Error: {$data["errorMessage"]}");
104 | }
105 | }
106 |
107 | return $this->generating;
108 | }
109 |
110 | public function getRemainingBoosts()
111 | {
112 | $data = Tools::request("https://www.bing.com/images/create", [
113 | 'cookie: _U=' . $this->cookie,
114 | 'method: GET',
115 | 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
116 | "accept-language: en;q=0.9",
117 | ]);
118 |
119 | if (! is_string($data) || ! preg_match('/
([0-9]+)<\/div>/', $data, $matches))
120 | return 0;
121 |
122 | return intval($matches[1]);
123 | }
124 |
125 | public function wait()
126 | {
127 | while (! $this->hasFailed()) {
128 | Tools::debug("Waiting for images generation...");
129 | sleep(1);
130 |
131 | if (! $this->isGenerating())
132 | break;
133 | }
134 | }
135 |
136 | public function getImages()
137 | {
138 | return $this->images;
139 | }
140 |
141 | public function getGenerationId()
142 | {
143 | return $this->generation_id;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | __New feature: send images in chat 🔥__
2 |
3 | 
4 |
5 | # Bing AI client
6 |
7 | [](https://opensource.org/licenses/MIT)
8 | [](https://packagist.org/packages/maximerenou/bing-ai)
9 | [](https://packagist.org/packages/maximerenou/bing-ai)
10 | [](https://packagist.org/packages/maximerenou/bing-ai)
11 |
12 | This is an unofficial PHP client for **Bing AI**, including **Chat (GPT-4)** and **Image Creator (DALL-E)**.
13 |
14 | ## Installation
15 |
16 | composer require maximerenou/bing-ai
17 |
18 | ## Demo
19 |
20 | This demo program uses both Chat and Image Creator:
21 |
22 | 
23 |
24 | Source: `examples/multi.php`.
25 |
26 | ## Usage
27 |
28 | - [Chat AI](#chat-ai)
29 | - [Image Creator AI](#image-creator)
30 |
31 | First, you need to sign in on bing.com and get your `_U` cookie.
32 |
33 | > ❗ Make sure you send a first message to Bing Chat before using your cookie (CAPTCHA bypass)
34 |
35 |
36 | How to get this cookie?
37 |
38 | 1. Navigate to bing.com
39 | 2. Sign in using your Microsoft account
40 | 3. Back on bing.com, go to Bing Chat page and send a message (CAPTCHA verification occurs sometimes)
41 | 4. Then, right-click and select "Inspect" - the browser console appears
42 | 4. Go to "Application" tab
43 | 5. Select "Cookies" > "https://www.bing.com" in the sidebar
44 | 6. Search for "_U" cookie
45 | 7. Copy its content
46 |
47 |
48 |
49 |
50 | How to check if my cookie is working properly?
51 |
52 | ```php
53 | use MaximeRenou\BingAI\BingAI;
54 |
55 | // $cookie - your "_U" cookie from bing.com
56 | $ai = new BingAI($cookie);
57 | $valid = $ai->checkCookie();
58 | ```
59 |
60 |
61 |
62 | ---------------------------------------
63 |
64 | ### Chat AI
65 |
66 | **Demo**: clone this repo, edit and run `examples/chat.php`.
67 |
68 | ```php
69 | use MaximeRenou\BingAI\BingAI;
70 | use MaximeRenou\BingAI\Chat\Prompt;
71 |
72 | // $cookie - your "_U" cookie from bing.com
73 | $ai = new BingAI($cookie);
74 |
75 | $conversation = $ai->createChatConversation();
76 |
77 | // $text - Text-only version of Bing's answer
78 | // $cards - Message objects array
79 | list($text, $cards) = $conversation->ask(new Prompt("Hello World"));
80 | ```
81 |
82 | > `$cards` contains all "messages" exchanged with Bing AI. It can be text (prompt or answer), signals, suggestions, image generation requests, etc. Check `Message.php` to learn more
83 | about its format.
84 |
85 |
86 | 🔥 Image analysis
87 |
88 | You may attach an image to your message:
89 |
90 | ```php
91 | $prompt = new Prompt("How cute is this animal?");
92 | $prompt->withImage('/path/to/panda.png');
93 | //or: $prompt->withImage($raw_image_data, true);
94 |
95 | $conversation->ask($prompt, ...);
96 | ```
97 |
98 | > **Try it!** Type _`$image`_ at the end of your message with `examples/chat.php`.
99 |
100 |
101 |
102 |
103 | Real-time / progressive answer
104 |
105 | You may pass a function as second argument to get real-time progression:
106 |
107 | ```php
108 | // $text - Incomplete text version
109 | // $cards - Incomplete messages fleet
110 | list($final_text, $final_cards) = $conversation->ask($prompt, function ($text, $cards) {
111 | echo $text;
112 | });
113 | ```
114 |
115 |
116 |
117 |
118 | Locale and location preferences
119 |
120 | ```php
121 | $conversation = $ai->createChatConversation()
122 | ->withLocation($latitude, $longitude, $radius) // Optional
123 | ->withPreferences('fr-FR', 'fr-FR', 'FR'); // Optional
124 | ```
125 |
126 |
127 |
128 |
129 | Tone choice
130 |
131 | ```php
132 | use MaximeRenou\BingAI\Chat\Tone;
133 |
134 | $conversation = $ai->createChatConversation()
135 | ->withTone(Tone::Creative); // Optional
136 |
137 | // Choices:
138 | // Tone::Balanced (default)
139 | // Tone::Creative
140 | // Tone::Precise
141 | ```
142 |
143 |
144 |
145 |
146 | Resume a conversation
147 |
148 | If you want to resume a previous conversation, you can retrieve its identifiers:
149 |
150 | ```php
151 | // Get current identifiers
152 | $identifiers = $conversation->getIdentifiers();
153 |
154 | // ...
155 | // Resume conversation with $identifiers parameter, and number of previous questions asked
156 | $conversation = $ai->resumeChatConversation($identifiers, 1);
157 | ```
158 |
159 |
160 |
161 |
162 | Text generation
163 |
164 | ```php
165 | $subject = "Internet memes";
166 | $tone = 'funny';
167 | $type = 'blog post';
168 | $length = 'short';
169 |
170 | $prompt = new Prompt("Please write a *$length* *$type* in a *$tone* style about `$subject`. Please wrap the $type in a markdown codeblock.");
171 |
172 | $conversation->ask($prompt->withoutCache(), ...)
173 | ```
174 |
175 | > To prevent answers like "I have already written \[...]", you can disable cache for your prompt with `withoutCache()`.
176 |
177 |
178 |
179 |
180 | Handle throttling and kicks
181 |
182 | Bing is limiting messages count per conversations. You can monitor it by calling `getRemainingMessages()` after every interaction.
183 |
184 | ```php
185 | $remaining = $conversation->getRemainingMessages();
186 |
187 | if ($remaining === 0) {
188 | // You reached the limit
189 | }
190 | ```
191 |
192 | After every interaction, you should also check if you have been kicked from the conversation:
193 |
194 | ```php
195 | if ($conversation->kicked()) {
196 | // You have been kicked, you should start a new conversation
197 | }
198 | ```
199 |
200 | You may combine both checks with:
201 |
202 | ```php
203 | if ($conversation->ended()) {
204 | // You reached the limit or have been kicked
205 | }
206 | ```
207 |
208 |
209 |
210 | ---------------------------------------
211 |
212 | ### Image Creator
213 |
214 | **Demo**: clone this repo, edit and run `examples/images.php`.
215 |
216 | ```php
217 | use MaximeRenou\BingAI\BingAI;
218 |
219 | // $cookie - your "_U" cookie from bing.com
220 | $ai = new BingAI($cookie);
221 |
222 | $creator = $ai->createImages("A 3D teddy bear");
223 |
224 | $creator->wait();
225 |
226 | // Finally, get images URLs
227 | if (! $creator->hasFailed()) {
228 | $images = $creator->getImages();
229 | }
230 | ```
231 |
232 | > Image generation can become slower after consuming all of your "boosts". Check the section below to stay aware of your remaining boosts.
233 |
234 |
235 | Check remaining boosts
236 |
237 | ```php
238 | $creator = $ai->getImageCreator();
239 |
240 | $remaining_boosts = $creator->getRemainingBoosts();
241 | ```
242 |
243 |
244 |
245 |
246 | Asynchronous generation
247 | You may quit after calling `createImages()` and check generation later using its ID:
248 |
249 | ```php
250 | $prompt = "A 3D teddy bear";
251 | $creator = $ai->createImages($prompt);
252 | $generation_id = $creator->getGenerationId();
253 |
254 | // ...
255 |
256 | $creator = $ai->getImageCreator();
257 | $creator->resume($generation_id, $prompt);
258 | ```
259 |
260 |
261 |
262 |
263 | Manually wait
264 | Instead of calling `$creator->wait();` you can loop by yourself:
265 |
266 | ```php
267 | do {
268 | sleep(1);
269 | } while ($creator->isGenerating());
270 | ```
271 |
272 |
273 |
274 | ---------------------------------------
275 |
276 | #### Disclaimer
277 |
278 | Using Bing AI outside bing.com may violate Bing terms. Use it at your own risk.
279 | Bing is a trademark of Microsoft.
280 |
--------------------------------------------------------------------------------
/src/Chat/Conversation.php:
--------------------------------------------------------------------------------
1 | cookie = $cookie;
37 |
38 | if (! is_array($identifiers))
39 | $identifiers = $this->createIdentifiers();
40 |
41 | $this->id = $identifiers['conversationId'];
42 | $this->client_id = $identifiers['clientId'];
43 | $this->signature = $identifiers['conversationSignature'];
44 | $this->invocations = $invocations - 1;
45 | }
46 |
47 | public function getIdentifiers()
48 | {
49 | return [
50 | 'conversationId' => $this->id,
51 | 'clientId' => $this->client_id,
52 | 'conversationSignature' => $this->signature
53 | ];
54 | }
55 |
56 | public function withTone($tone)
57 | {
58 | $this->tone = $tone;
59 | return $this;
60 | }
61 |
62 | public function withPreferences($locale = 'en-US', $market = 'en-US', $region = 'US')
63 | {
64 | $this->locale = $locale;
65 | $this->market = $market;
66 | $this->region = $region;
67 | return $this;
68 | }
69 |
70 | public function withLocation($latitude, $longitude, $radius = 1000)
71 | {
72 | $this->geolocation = [$latitude, $longitude, $radius];
73 | return $this;
74 | }
75 |
76 | public function getRemainingMessages()
77 | {
78 | if (is_null($this->max_messages_count))
79 | return 1;
80 |
81 | return $this->max_messages_count - $this->user_messages_count;
82 | }
83 |
84 | public function kicked()
85 | {
86 | return $this->kicked;
87 | }
88 |
89 | public function ended()
90 | {
91 | return $this->kicked || $this->getRemainingMessages() <= 0;
92 | }
93 |
94 | public function createIdentifiers()
95 | {
96 | $data = Tools::request("https://www.bing.com/turing/conversation/create", [
97 | 'cookie: _U=' . $this->cookie,
98 | 'method: GET',
99 | 'accept: application/json',
100 | "accept-language: {$this->region},{$this->locale};q=0.9",
101 | 'content-type: application/json',
102 | 'x-ms-client-request-id' => Tools::generateUUID(),
103 | ]);
104 |
105 | $data = json_decode($data, true);
106 |
107 | if (! is_array($data) || ! isset($data['result']) || ! isset($data['result']['value']) || $data['result']['value'] != 'Success')
108 | throw new \Exception("Failed to init conversation");
109 |
110 | return $data;
111 | }
112 |
113 | public function uploadImage($image_data)
114 | {
115 | $image_encoded = base64_encode($image_data);
116 |
117 | $form = [
118 | 'knowledgeRequest' => json_encode([
119 | "imageInfo" => (object) [],
120 | "knowledgeRequest" => [
121 | "invokedSkills" => ["ImageById"],
122 | "subscriptionId" => "Bing.Chat.Multimodal",
123 | "invokedSkillsRequestData" => [
124 | "enableFaceBlur" => true
125 | ],
126 | "convoData" => [
127 | "convoid" => '', // $this->id ?
128 | "convotone" => 'Precise' // $this->tone ?
129 | ]
130 | ]
131 | ]),
132 | 'imageBase64' => $image_encoded
133 | ];
134 |
135 | Tools::debug("Image upload request: " . print_r($form, true));
136 |
137 | $data = Tools::request("https://www.bing.com/images/kblob", [
138 | 'cookie: _U=' . $this->cookie,
139 | 'method: POST',
140 | 'Content-Type: multipart/form-data',
141 | 'referer: https://www.bing.com',
142 | ], $form);
143 |
144 | Tools::debug("Image upload response: $data");
145 |
146 | $data = json_decode($data, true);
147 |
148 | if (! is_array($data) || empty($data['blobId']) || empty($data['processedBlobId']))
149 | throw new \Exception("Failed to upload image");
150 |
151 | return [
152 | 'originalImageUrl' => 'https://www.bing.com/images/blob?bcid=' . $data['blobId'],
153 | 'imageUrl' => 'https://www.bing.com/images/blob?bcid=' . $data['processedBlobId'],
154 | ];
155 | }
156 |
157 | public function ask(Prompt $message, $callback = null)
158 | {
159 | $this->invocations++;
160 | $this->current_started = false;
161 | $this->current_text = '';
162 |
163 | $this->current_messages = [
164 | Message::fromPrompt($message)
165 | ];
166 |
167 | $headers = [
168 | 'accept-language' => $this->locale . ',' . $this->region . ';q=0.9',
169 | 'cache-control' => 'no-cache',
170 | 'pragma' => 'no-cache'
171 | ];
172 |
173 | Tools::debug("Creating loop");
174 |
175 | \React\EventLoop\Loop::set(\React\EventLoop\Factory::create());
176 |
177 | $loop = \React\EventLoop\Loop::get();
178 |
179 | \Ratchet\Client\connect('wss://sydney.bing.com/sydney/ChatHub', [], $headers, $loop)->then(function ($connection) use ($message, $callback) {
180 | Tools::debug("Connection open");
181 |
182 | $connection->on('message', function ($raw) use ($connection, $message, $callback) {
183 | Tools::debug("Packet received");
184 | Tools::debug($raw);
185 | $this->handlePacket($raw, $connection, $message, $callback);
186 | });
187 |
188 | $connection->on('close', function () {
189 | Tools::debug("Connection closed");
190 | });
191 |
192 | Tools::debug("Sending first packet");
193 | $connection->send(json_encode(['protocol' => 'json', 'version' => 1]) . self::END_CHAR);
194 | }, function ($error) {
195 | throw new \Exception($error->getMessage());
196 | });
197 |
198 | $loop->run();
199 |
200 | return [$this->current_text, $this->current_messages];
201 | }
202 |
203 | public function handlePacket($raw, $connection, $message, $callback)
204 | {
205 | $objects = explode(self::END_CHAR, $raw);
206 |
207 | $objects = array_map(function ($object) {
208 | return json_decode($object, true);
209 | }, $objects);
210 |
211 | $objects = array_filter($objects, function ($value) {
212 | return is_array($value);
213 | });
214 |
215 | if (count($objects) === 0) {
216 | return;
217 | }
218 |
219 | if (! $this->current_started) {
220 | Tools::debug("Sending start ping");
221 | $connection->send(json_encode(['type' => 6]) . self::END_CHAR);
222 |
223 | $trace_id = bin2hex(random_bytes(16));
224 | $location = null;
225 |
226 | if (is_array($this->geolocation)) {
227 | $location = "lat:{$this->geolocation[0]};long={$this->geolocation[1]};re={$this->geolocation[2]}m;";
228 | // Example format: "lat:47.639557;long:-122.128159;re=1000m;";
229 | }
230 |
231 | $locationHints = [
232 | // Example hint
233 | /*[
234 | "country" => "France",
235 | "timezoneoffset" => 1,
236 | "countryConfidence" => 9,
237 | "Center" => [
238 | "Latitude" => 48,
239 | "Longitude" => 1
240 | ],
241 | "RegionType" => 2,
242 | "SourceType" => 1
243 | ]*/
244 | ];
245 |
246 | $options = [
247 | 'nlu_direct_response_filter',
248 | 'deepleo',
249 | 'enable_debug_commands',
250 | 'disable_emoji_spoken_text',
251 | 'responsible_ai_policy_235',
252 | 'enablemm',
253 |
254 | // V2 options
255 | 'enbfpr',
256 | 'jb095',
257 | 'jbfv1',
258 | 'nojbfedge',
259 | 'weasgv2',
260 | 'dv3sugg',
261 | 'inputlanguage',
262 | 'rediscluster',
263 |
264 | // V3 options
265 | "iyxapbing",
266 | "iycapbing",
267 | "enpcktrk",
268 | "logosv1",
269 | "iyolojb",
270 | ];
271 |
272 |
273 | if ($this->tone === Tone::Creative) {
274 | $options = array_merge($options, [
275 | 'h3imaginative',
276 | 'clgalileo',
277 | 'gencontentv3',
278 | 'gencontentv5'
279 | ]);
280 | }
281 | elseif ($this->tone === Tone::Precise) {
282 | $options = array_merge($options, [
283 | 'h3precise',
284 | 'clgalileo',
285 | 'gencontentv5',
286 |
287 | // V3 options
288 | 'gencontentv3',
289 | ]);
290 | }
291 | else {
292 | $options = array_merge($options, [
293 | 'galileo',
294 | 'visualcreative',
295 |
296 | // V3 options
297 | 'harmonyv3',
298 | 'saharagenconv5',
299 | ]);
300 | }
301 |
302 | if (! $message->cache) {
303 | $options[] = "nocache";
304 | }
305 |
306 | $message_data = [
307 | 'locale' => $message->locale ?? $this->locale,
308 | 'market' => $message->market ?? $this->market,
309 | 'region' => $message->region ?? $this->region,
310 | 'location' => $location,
311 | 'locationHints' => $locationHints,
312 | 'timestamp' => date('Y-m-d') . 'T' . date('H:i:sP'),
313 | 'author' => 'user',
314 | 'inputMethod' => 'Keyboard',
315 | 'text' => $message->text,
316 | 'messageType' => 'Chat',
317 | ];
318 |
319 | if (! empty($message->image)) {
320 | $message_data = array_merge($message_data, $this->uploadImage($message->image));
321 | $message->image = null;
322 | }
323 |
324 | $params = [
325 | 'arguments' => [
326 | [
327 | 'source' => 'cib',
328 | 'verbosity' => 'verbose',
329 | 'scenario' => 'SERP',
330 | 'optionsSets' => $options,
331 | 'allowedMessageTypes' => [
332 | 'Chat',
333 | 'InternalSearchQuery',
334 | 'InternalSearchResult',
335 | 'InternalLoaderMessage',
336 | 'RenderCardRequest',
337 | 'AdsQuery',
338 | 'SemanticSerp',
339 | 'Disengaged',
340 | 'ActionRequest',
341 | 'GenerateContentQuery',
342 | 'SearchQuery',
343 |
344 | // V3
345 | 'Context',
346 | 'Progress',
347 | ],
348 | 'sliceIds' => [],
349 | 'traceId' => $trace_id,
350 | 'isStartOfSession' => $this->invocations == 0,
351 | 'message' => $message_data,
352 | 'conversationSignature' => $this->signature,
353 | 'participant' => ['id' => $this->client_id],
354 | 'conversationId' => $this->id,
355 | 'spokenTextMode' => 'None',
356 | ]
357 | ],
358 | 'invocationId' => "{$this->invocations}",
359 | 'target' => 'chat',
360 | 'type' => 4
361 | ];
362 |
363 | $connection->send(json_encode($params) . self::END_CHAR);
364 |
365 | $this->current_started = true;
366 |
367 | return;
368 | }
369 |
370 | foreach ($objects as $object) {
371 | if ($this->handleObject($object, $callback))
372 | $connection->close();
373 | }
374 | }
375 |
376 | public function handleObject($object, $callback = null)
377 | {
378 | $terminate = false;
379 |
380 | switch ($object['type'])
381 | {
382 | case 1: // Partial result
383 | $messages = [];
384 |
385 | foreach ($object['arguments'] as $argument) {
386 | if (isset($argument['messages']) && is_array($argument['messages'])) {
387 | foreach ($argument['messages'] as $messageData) {
388 | $messages[] = Message::fromData($messageData);
389 | }
390 | }
391 |
392 | if (isset($argument['throttling']) && is_array($argument['throttling'])) {
393 | if (isset($argument['throttling']['maxNumUserMessagesInConversation'])) {
394 | $this->max_messages_count = $argument['throttling']['maxNumUserMessagesInConversation'];
395 | }
396 |
397 | if (isset($argument['throttling']['numUserMessagesInConversation'])) {
398 | $this->user_messages_count = $argument['throttling']['numUserMessagesInConversation'];
399 | }
400 | }
401 | }
402 |
403 | foreach ($this->current_messages as $previous_message) {
404 | $older = true;
405 |
406 | foreach ($messages as $i => $message) {
407 | if ($message->id == $previous_message->id) {
408 | $older = false;
409 | break;
410 | }
411 | }
412 |
413 | if ($older) {
414 | array_unshift($messages, $previous_message);
415 | }
416 | }
417 |
418 | $this->current_messages = $messages;
419 | break;
420 | case 2: // Global result
421 | $this->current_messages = [];
422 |
423 | if (isset($object['item']['messages']) && is_array($object['item']['messages'])) {
424 | foreach ($object['item']['messages'] as $messageData) {
425 | $this->current_messages[] = Message::fromData($messageData);
426 | }
427 | }
428 |
429 | if (isset($object['item']['throttling']) && is_array($object['item']['throttling'])) {
430 | if (isset($object['item']['throttling']['maxNumUserMessagesInConversation'])) {
431 | $this->max_messages_count = $object['item']['throttling']['maxNumUserMessagesInConversation'];
432 | }
433 |
434 | if (isset($object['item']['throttling']['numUserMessagesInConversation'])) {
435 | $this->user_messages_count = $object['item']['throttling']['numUserMessagesInConversation'];
436 | }
437 | }
438 | break;
439 | case 3: // Answer ended
440 | // Available: $object['invocationId'];
441 | $terminate = true;
442 | break;
443 | case 6:
444 | // Ping
445 | return $terminate;
446 | }
447 |
448 | $text_parts = [];
449 |
450 | foreach ($this->current_messages as $message) {
451 | if ($message->type == MessageType::Answer)
452 | $text_parts[] = $message->toText();
453 | elseif ($message->type == MessageType::Disengaged)
454 | $this->kicked = true;
455 | }
456 |
457 | $this->current_text = trim(implode('. ', array_filter($text_parts)));
458 |
459 | // Callback
460 |
461 | if (! is_null($callback)) {
462 | call_user_func_array($callback, [$this->current_text, $this->current_messages]);
463 | }
464 |
465 | return $terminate;
466 | }
467 | }
468 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "01757a5ae48b5afd1a53a2d727e20bfe",
8 | "packages": [
9 | {
10 | "name": "evenement/evenement",
11 | "version": "v3.0.1",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/igorw/evenement.git",
15 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7",
20 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": ">=7.0"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^6.0"
28 | },
29 | "type": "library",
30 | "autoload": {
31 | "psr-0": {
32 | "Evenement": "src"
33 | }
34 | },
35 | "notification-url": "https://packagist.org/downloads/",
36 | "license": [
37 | "MIT"
38 | ],
39 | "authors": [
40 | {
41 | "name": "Igor Wiedler",
42 | "email": "igor@wiedler.ch"
43 | }
44 | ],
45 | "description": "Événement is a very simple event dispatching library for PHP",
46 | "keywords": [
47 | "event-dispatcher",
48 | "event-emitter"
49 | ],
50 | "support": {
51 | "issues": "https://github.com/igorw/evenement/issues",
52 | "source": "https://github.com/igorw/evenement/tree/master"
53 | },
54 | "time": "2017-07-23T21:35:13+00:00"
55 | },
56 | {
57 | "name": "guzzlehttp/psr7",
58 | "version": "1.9.1",
59 | "source": {
60 | "type": "git",
61 | "url": "https://github.com/guzzle/psr7.git",
62 | "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
63 | },
64 | "dist": {
65 | "type": "zip",
66 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
67 | "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
68 | "shasum": ""
69 | },
70 | "require": {
71 | "php": ">=5.4.0",
72 | "psr/http-message": "~1.0",
73 | "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
74 | },
75 | "provide": {
76 | "psr/http-message-implementation": "1.0"
77 | },
78 | "require-dev": {
79 | "ext-zlib": "*",
80 | "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
81 | },
82 | "suggest": {
83 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
84 | },
85 | "type": "library",
86 | "autoload": {
87 | "files": [
88 | "src/functions_include.php"
89 | ],
90 | "psr-4": {
91 | "GuzzleHttp\\Psr7\\": "src/"
92 | }
93 | },
94 | "notification-url": "https://packagist.org/downloads/",
95 | "license": [
96 | "MIT"
97 | ],
98 | "authors": [
99 | {
100 | "name": "Graham Campbell",
101 | "email": "hello@gjcampbell.co.uk",
102 | "homepage": "https://github.com/GrahamCampbell"
103 | },
104 | {
105 | "name": "Michael Dowling",
106 | "email": "mtdowling@gmail.com",
107 | "homepage": "https://github.com/mtdowling"
108 | },
109 | {
110 | "name": "George Mponos",
111 | "email": "gmponos@gmail.com",
112 | "homepage": "https://github.com/gmponos"
113 | },
114 | {
115 | "name": "Tobias Nyholm",
116 | "email": "tobias.nyholm@gmail.com",
117 | "homepage": "https://github.com/Nyholm"
118 | },
119 | {
120 | "name": "Márk Sági-Kazár",
121 | "email": "mark.sagikazar@gmail.com",
122 | "homepage": "https://github.com/sagikazarmark"
123 | },
124 | {
125 | "name": "Tobias Schultze",
126 | "email": "webmaster@tubo-world.de",
127 | "homepage": "https://github.com/Tobion"
128 | }
129 | ],
130 | "description": "PSR-7 message implementation that also provides common utility methods",
131 | "keywords": [
132 | "http",
133 | "message",
134 | "psr-7",
135 | "request",
136 | "response",
137 | "stream",
138 | "uri",
139 | "url"
140 | ],
141 | "support": {
142 | "issues": "https://github.com/guzzle/psr7/issues",
143 | "source": "https://github.com/guzzle/psr7/tree/1.9.1"
144 | },
145 | "funding": [
146 | {
147 | "url": "https://github.com/GrahamCampbell",
148 | "type": "github"
149 | },
150 | {
151 | "url": "https://github.com/Nyholm",
152 | "type": "github"
153 | },
154 | {
155 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
156 | "type": "tidelift"
157 | }
158 | ],
159 | "time": "2023-04-17T16:00:37+00:00"
160 | },
161 | {
162 | "name": "psr/http-message",
163 | "version": "1.0.1",
164 | "source": {
165 | "type": "git",
166 | "url": "https://github.com/php-fig/http-message.git",
167 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
168 | },
169 | "dist": {
170 | "type": "zip",
171 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
172 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
173 | "shasum": ""
174 | },
175 | "require": {
176 | "php": ">=5.3.0"
177 | },
178 | "type": "library",
179 | "extra": {
180 | "branch-alias": {
181 | "dev-master": "1.0.x-dev"
182 | }
183 | },
184 | "autoload": {
185 | "psr-4": {
186 | "Psr\\Http\\Message\\": "src/"
187 | }
188 | },
189 | "notification-url": "https://packagist.org/downloads/",
190 | "license": [
191 | "MIT"
192 | ],
193 | "authors": [
194 | {
195 | "name": "PHP-FIG",
196 | "homepage": "http://www.php-fig.org/"
197 | }
198 | ],
199 | "description": "Common interface for HTTP messages",
200 | "homepage": "https://github.com/php-fig/http-message",
201 | "keywords": [
202 | "http",
203 | "http-message",
204 | "psr",
205 | "psr-7",
206 | "request",
207 | "response"
208 | ],
209 | "support": {
210 | "source": "https://github.com/php-fig/http-message/tree/master"
211 | },
212 | "time": "2016-08-06T14:39:51+00:00"
213 | },
214 | {
215 | "name": "ralouphie/getallheaders",
216 | "version": "3.0.3",
217 | "source": {
218 | "type": "git",
219 | "url": "https://github.com/ralouphie/getallheaders.git",
220 | "reference": "120b605dfeb996808c31b6477290a714d356e822"
221 | },
222 | "dist": {
223 | "type": "zip",
224 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
225 | "reference": "120b605dfeb996808c31b6477290a714d356e822",
226 | "shasum": ""
227 | },
228 | "require": {
229 | "php": ">=5.6"
230 | },
231 | "require-dev": {
232 | "php-coveralls/php-coveralls": "^2.1",
233 | "phpunit/phpunit": "^5 || ^6.5"
234 | },
235 | "type": "library",
236 | "autoload": {
237 | "files": [
238 | "src/getallheaders.php"
239 | ]
240 | },
241 | "notification-url": "https://packagist.org/downloads/",
242 | "license": [
243 | "MIT"
244 | ],
245 | "authors": [
246 | {
247 | "name": "Ralph Khattar",
248 | "email": "ralph.khattar@gmail.com"
249 | }
250 | ],
251 | "description": "A polyfill for getallheaders.",
252 | "support": {
253 | "issues": "https://github.com/ralouphie/getallheaders/issues",
254 | "source": "https://github.com/ralouphie/getallheaders/tree/develop"
255 | },
256 | "time": "2019-03-08T08:55:37+00:00"
257 | },
258 | {
259 | "name": "ratchet/pawl",
260 | "version": "v0.4.1",
261 | "source": {
262 | "type": "git",
263 | "url": "https://github.com/ratchetphp/Pawl.git",
264 | "reference": "af70198bab77a582b31169d3cc3982bed25c161f"
265 | },
266 | "dist": {
267 | "type": "zip",
268 | "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/af70198bab77a582b31169d3cc3982bed25c161f",
269 | "reference": "af70198bab77a582b31169d3cc3982bed25c161f",
270 | "shasum": ""
271 | },
272 | "require": {
273 | "evenement/evenement": "^3.0 || ^2.0",
274 | "guzzlehttp/psr7": "^2.0 || ^1.7",
275 | "php": ">=5.4",
276 | "ratchet/rfc6455": "^0.3.1",
277 | "react/socket": "^1.9"
278 | },
279 | "require-dev": {
280 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8"
281 | },
282 | "suggest": {
283 | "reactivex/rxphp": "~2.0"
284 | },
285 | "type": "library",
286 | "autoload": {
287 | "files": [
288 | "src/functions_include.php"
289 | ],
290 | "psr-4": {
291 | "Ratchet\\Client\\": "src"
292 | }
293 | },
294 | "notification-url": "https://packagist.org/downloads/",
295 | "license": [
296 | "MIT"
297 | ],
298 | "description": "Asynchronous WebSocket client",
299 | "keywords": [
300 | "Ratchet",
301 | "async",
302 | "client",
303 | "websocket",
304 | "websocket client"
305 | ],
306 | "support": {
307 | "issues": "https://github.com/ratchetphp/Pawl/issues",
308 | "source": "https://github.com/ratchetphp/Pawl/tree/v0.4.1"
309 | },
310 | "time": "2021-12-10T14:32:34+00:00"
311 | },
312 | {
313 | "name": "ratchet/rfc6455",
314 | "version": "v0.3.1",
315 | "source": {
316 | "type": "git",
317 | "url": "https://github.com/ratchetphp/RFC6455.git",
318 | "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb"
319 | },
320 | "dist": {
321 | "type": "zip",
322 | "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb",
323 | "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb",
324 | "shasum": ""
325 | },
326 | "require": {
327 | "guzzlehttp/psr7": "^2 || ^1.7",
328 | "php": ">=5.4.2"
329 | },
330 | "require-dev": {
331 | "phpunit/phpunit": "^5.7",
332 | "react/socket": "^1.3"
333 | },
334 | "type": "library",
335 | "autoload": {
336 | "psr-4": {
337 | "Ratchet\\RFC6455\\": "src"
338 | }
339 | },
340 | "notification-url": "https://packagist.org/downloads/",
341 | "license": [
342 | "MIT"
343 | ],
344 | "authors": [
345 | {
346 | "name": "Chris Boden",
347 | "email": "cboden@gmail.com",
348 | "role": "Developer"
349 | },
350 | {
351 | "name": "Matt Bonneau",
352 | "role": "Developer"
353 | }
354 | ],
355 | "description": "RFC6455 WebSocket protocol handler",
356 | "homepage": "http://socketo.me",
357 | "keywords": [
358 | "WebSockets",
359 | "rfc6455",
360 | "websocket"
361 | ],
362 | "support": {
363 | "chat": "https://gitter.im/reactphp/reactphp",
364 | "issues": "https://github.com/ratchetphp/RFC6455/issues",
365 | "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1"
366 | },
367 | "time": "2021-12-09T23:20:49+00:00"
368 | },
369 | {
370 | "name": "react/cache",
371 | "version": "v1.2.0",
372 | "source": {
373 | "type": "git",
374 | "url": "https://github.com/reactphp/cache.git",
375 | "reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
376 | },
377 | "dist": {
378 | "type": "zip",
379 | "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
380 | "reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
381 | "shasum": ""
382 | },
383 | "require": {
384 | "php": ">=5.3.0",
385 | "react/promise": "^3.0 || ^2.0 || ^1.1"
386 | },
387 | "require-dev": {
388 | "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
389 | },
390 | "type": "library",
391 | "autoload": {
392 | "psr-4": {
393 | "React\\Cache\\": "src/"
394 | }
395 | },
396 | "notification-url": "https://packagist.org/downloads/",
397 | "license": [
398 | "MIT"
399 | ],
400 | "authors": [
401 | {
402 | "name": "Christian Lück",
403 | "email": "christian@clue.engineering",
404 | "homepage": "https://clue.engineering/"
405 | },
406 | {
407 | "name": "Cees-Jan Kiewiet",
408 | "email": "reactphp@ceesjankiewiet.nl",
409 | "homepage": "https://wyrihaximus.net/"
410 | },
411 | {
412 | "name": "Jan Sorgalla",
413 | "email": "jsorgalla@gmail.com",
414 | "homepage": "https://sorgalla.com/"
415 | },
416 | {
417 | "name": "Chris Boden",
418 | "email": "cboden@gmail.com",
419 | "homepage": "https://cboden.dev/"
420 | }
421 | ],
422 | "description": "Async, Promise-based cache interface for ReactPHP",
423 | "keywords": [
424 | "cache",
425 | "caching",
426 | "promise",
427 | "reactphp"
428 | ],
429 | "support": {
430 | "issues": "https://github.com/reactphp/cache/issues",
431 | "source": "https://github.com/reactphp/cache/tree/v1.2.0"
432 | },
433 | "funding": [
434 | {
435 | "url": "https://opencollective.com/reactphp",
436 | "type": "open_collective"
437 | }
438 | ],
439 | "time": "2022-11-30T15:59:55+00:00"
440 | },
441 | {
442 | "name": "react/dns",
443 | "version": "v1.10.0",
444 | "source": {
445 | "type": "git",
446 | "url": "https://github.com/reactphp/dns.git",
447 | "reference": "a5427e7dfa47713e438016905605819d101f238c"
448 | },
449 | "dist": {
450 | "type": "zip",
451 | "url": "https://api.github.com/repos/reactphp/dns/zipball/a5427e7dfa47713e438016905605819d101f238c",
452 | "reference": "a5427e7dfa47713e438016905605819d101f238c",
453 | "shasum": ""
454 | },
455 | "require": {
456 | "php": ">=5.3.0",
457 | "react/cache": "^1.0 || ^0.6 || ^0.5",
458 | "react/event-loop": "^1.2",
459 | "react/promise": "^3.0 || ^2.7 || ^1.2.1",
460 | "react/promise-timer": "^1.9"
461 | },
462 | "require-dev": {
463 | "phpunit/phpunit": "^9.3 || ^4.8.35",
464 | "react/async": "^4 || ^3 || ^2"
465 | },
466 | "type": "library",
467 | "autoload": {
468 | "psr-4": {
469 | "React\\Dns\\": "src"
470 | }
471 | },
472 | "notification-url": "https://packagist.org/downloads/",
473 | "license": [
474 | "MIT"
475 | ],
476 | "authors": [
477 | {
478 | "name": "Christian Lück",
479 | "email": "christian@clue.engineering",
480 | "homepage": "https://clue.engineering/"
481 | },
482 | {
483 | "name": "Cees-Jan Kiewiet",
484 | "email": "reactphp@ceesjankiewiet.nl",
485 | "homepage": "https://wyrihaximus.net/"
486 | },
487 | {
488 | "name": "Jan Sorgalla",
489 | "email": "jsorgalla@gmail.com",
490 | "homepage": "https://sorgalla.com/"
491 | },
492 | {
493 | "name": "Chris Boden",
494 | "email": "cboden@gmail.com",
495 | "homepage": "https://cboden.dev/"
496 | }
497 | ],
498 | "description": "Async DNS resolver for ReactPHP",
499 | "keywords": [
500 | "async",
501 | "dns",
502 | "dns-resolver",
503 | "reactphp"
504 | ],
505 | "support": {
506 | "issues": "https://github.com/reactphp/dns/issues",
507 | "source": "https://github.com/reactphp/dns/tree/v1.10.0"
508 | },
509 | "funding": [
510 | {
511 | "url": "https://github.com/WyriHaximus",
512 | "type": "github"
513 | },
514 | {
515 | "url": "https://github.com/clue",
516 | "type": "github"
517 | }
518 | ],
519 | "time": "2022-09-08T12:22:46+00:00"
520 | },
521 | {
522 | "name": "react/event-loop",
523 | "version": "v1.3.0",
524 | "source": {
525 | "type": "git",
526 | "url": "https://github.com/reactphp/event-loop.git",
527 | "reference": "187fb56f46d424afb6ec4ad089269c72eec2e137"
528 | },
529 | "dist": {
530 | "type": "zip",
531 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/187fb56f46d424afb6ec4ad089269c72eec2e137",
532 | "reference": "187fb56f46d424afb6ec4ad089269c72eec2e137",
533 | "shasum": ""
534 | },
535 | "require": {
536 | "php": ">=5.3.0"
537 | },
538 | "require-dev": {
539 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
540 | },
541 | "suggest": {
542 | "ext-event": "~1.0 for ExtEventLoop",
543 | "ext-pcntl": "For signal handling support when using the StreamSelectLoop",
544 | "ext-uv": "* for ExtUvLoop"
545 | },
546 | "type": "library",
547 | "autoload": {
548 | "psr-4": {
549 | "React\\EventLoop\\": "src"
550 | }
551 | },
552 | "notification-url": "https://packagist.org/downloads/",
553 | "license": [
554 | "MIT"
555 | ],
556 | "authors": [
557 | {
558 | "name": "Christian Lück",
559 | "email": "christian@clue.engineering",
560 | "homepage": "https://clue.engineering/"
561 | },
562 | {
563 | "name": "Cees-Jan Kiewiet",
564 | "email": "reactphp@ceesjankiewiet.nl",
565 | "homepage": "https://wyrihaximus.net/"
566 | },
567 | {
568 | "name": "Jan Sorgalla",
569 | "email": "jsorgalla@gmail.com",
570 | "homepage": "https://sorgalla.com/"
571 | },
572 | {
573 | "name": "Chris Boden",
574 | "email": "cboden@gmail.com",
575 | "homepage": "https://cboden.dev/"
576 | }
577 | ],
578 | "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
579 | "keywords": [
580 | "asynchronous",
581 | "event-loop"
582 | ],
583 | "support": {
584 | "issues": "https://github.com/reactphp/event-loop/issues",
585 | "source": "https://github.com/reactphp/event-loop/tree/v1.3.0"
586 | },
587 | "funding": [
588 | {
589 | "url": "https://github.com/WyriHaximus",
590 | "type": "github"
591 | },
592 | {
593 | "url": "https://github.com/clue",
594 | "type": "github"
595 | }
596 | ],
597 | "time": "2022-03-17T11:10:22+00:00"
598 | },
599 | {
600 | "name": "react/promise",
601 | "version": "v2.9.0",
602 | "source": {
603 | "type": "git",
604 | "url": "https://github.com/reactphp/promise.git",
605 | "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910"
606 | },
607 | "dist": {
608 | "type": "zip",
609 | "url": "https://api.github.com/repos/reactphp/promise/zipball/234f8fd1023c9158e2314fa9d7d0e6a83db42910",
610 | "reference": "234f8fd1023c9158e2314fa9d7d0e6a83db42910",
611 | "shasum": ""
612 | },
613 | "require": {
614 | "php": ">=5.4.0"
615 | },
616 | "require-dev": {
617 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.36"
618 | },
619 | "type": "library",
620 | "autoload": {
621 | "files": [
622 | "src/functions_include.php"
623 | ],
624 | "psr-4": {
625 | "React\\Promise\\": "src/"
626 | }
627 | },
628 | "notification-url": "https://packagist.org/downloads/",
629 | "license": [
630 | "MIT"
631 | ],
632 | "authors": [
633 | {
634 | "name": "Jan Sorgalla",
635 | "email": "jsorgalla@gmail.com",
636 | "homepage": "https://sorgalla.com/"
637 | },
638 | {
639 | "name": "Christian Lück",
640 | "email": "christian@clue.engineering",
641 | "homepage": "https://clue.engineering/"
642 | },
643 | {
644 | "name": "Cees-Jan Kiewiet",
645 | "email": "reactphp@ceesjankiewiet.nl",
646 | "homepage": "https://wyrihaximus.net/"
647 | },
648 | {
649 | "name": "Chris Boden",
650 | "email": "cboden@gmail.com",
651 | "homepage": "https://cboden.dev/"
652 | }
653 | ],
654 | "description": "A lightweight implementation of CommonJS Promises/A for PHP",
655 | "keywords": [
656 | "promise",
657 | "promises"
658 | ],
659 | "support": {
660 | "issues": "https://github.com/reactphp/promise/issues",
661 | "source": "https://github.com/reactphp/promise/tree/v2.9.0"
662 | },
663 | "funding": [
664 | {
665 | "url": "https://github.com/WyriHaximus",
666 | "type": "github"
667 | },
668 | {
669 | "url": "https://github.com/clue",
670 | "type": "github"
671 | }
672 | ],
673 | "time": "2022-02-11T10:27:51+00:00"
674 | },
675 | {
676 | "name": "react/promise-timer",
677 | "version": "v1.9.0",
678 | "source": {
679 | "type": "git",
680 | "url": "https://github.com/reactphp/promise-timer.git",
681 | "reference": "aa7a73c74b8d8c0f622f5982ff7b0351bc29e495"
682 | },
683 | "dist": {
684 | "type": "zip",
685 | "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/aa7a73c74b8d8c0f622f5982ff7b0351bc29e495",
686 | "reference": "aa7a73c74b8d8c0f622f5982ff7b0351bc29e495",
687 | "shasum": ""
688 | },
689 | "require": {
690 | "php": ">=5.3",
691 | "react/event-loop": "^1.2",
692 | "react/promise": "^3.0 || ^2.7.0 || ^1.2.1"
693 | },
694 | "require-dev": {
695 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
696 | },
697 | "type": "library",
698 | "autoload": {
699 | "files": [
700 | "src/functions_include.php"
701 | ],
702 | "psr-4": {
703 | "React\\Promise\\Timer\\": "src/"
704 | }
705 | },
706 | "notification-url": "https://packagist.org/downloads/",
707 | "license": [
708 | "MIT"
709 | ],
710 | "authors": [
711 | {
712 | "name": "Christian Lück",
713 | "email": "christian@clue.engineering",
714 | "homepage": "https://clue.engineering/"
715 | },
716 | {
717 | "name": "Cees-Jan Kiewiet",
718 | "email": "reactphp@ceesjankiewiet.nl",
719 | "homepage": "https://wyrihaximus.net/"
720 | },
721 | {
722 | "name": "Jan Sorgalla",
723 | "email": "jsorgalla@gmail.com",
724 | "homepage": "https://sorgalla.com/"
725 | },
726 | {
727 | "name": "Chris Boden",
728 | "email": "cboden@gmail.com",
729 | "homepage": "https://cboden.dev/"
730 | }
731 | ],
732 | "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.",
733 | "homepage": "https://github.com/reactphp/promise-timer",
734 | "keywords": [
735 | "async",
736 | "event-loop",
737 | "promise",
738 | "reactphp",
739 | "timeout",
740 | "timer"
741 | ],
742 | "support": {
743 | "issues": "https://github.com/reactphp/promise-timer/issues",
744 | "source": "https://github.com/reactphp/promise-timer/tree/v1.9.0"
745 | },
746 | "funding": [
747 | {
748 | "url": "https://github.com/WyriHaximus",
749 | "type": "github"
750 | },
751 | {
752 | "url": "https://github.com/clue",
753 | "type": "github"
754 | }
755 | ],
756 | "time": "2022-06-13T13:41:03+00:00"
757 | },
758 | {
759 | "name": "react/socket",
760 | "version": "v1.12.0",
761 | "source": {
762 | "type": "git",
763 | "url": "https://github.com/reactphp/socket.git",
764 | "reference": "81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b"
765 | },
766 | "dist": {
767 | "type": "zip",
768 | "url": "https://api.github.com/repos/reactphp/socket/zipball/81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b",
769 | "reference": "81e1b4d7f5450ebd8d2e9a95bb008bb15ca95a7b",
770 | "shasum": ""
771 | },
772 | "require": {
773 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
774 | "php": ">=5.3.0",
775 | "react/dns": "^1.8",
776 | "react/event-loop": "^1.2",
777 | "react/promise": "^3 || ^2.6 || ^1.2.1",
778 | "react/promise-timer": "^1.9",
779 | "react/stream": "^1.2"
780 | },
781 | "require-dev": {
782 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
783 | "react/async": "^4 || ^3 || ^2",
784 | "react/promise-stream": "^1.4"
785 | },
786 | "type": "library",
787 | "autoload": {
788 | "psr-4": {
789 | "React\\Socket\\": "src"
790 | }
791 | },
792 | "notification-url": "https://packagist.org/downloads/",
793 | "license": [
794 | "MIT"
795 | ],
796 | "authors": [
797 | {
798 | "name": "Christian Lück",
799 | "email": "christian@clue.engineering",
800 | "homepage": "https://clue.engineering/"
801 | },
802 | {
803 | "name": "Cees-Jan Kiewiet",
804 | "email": "reactphp@ceesjankiewiet.nl",
805 | "homepage": "https://wyrihaximus.net/"
806 | },
807 | {
808 | "name": "Jan Sorgalla",
809 | "email": "jsorgalla@gmail.com",
810 | "homepage": "https://sorgalla.com/"
811 | },
812 | {
813 | "name": "Chris Boden",
814 | "email": "cboden@gmail.com",
815 | "homepage": "https://cboden.dev/"
816 | }
817 | ],
818 | "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
819 | "keywords": [
820 | "Connection",
821 | "Socket",
822 | "async",
823 | "reactphp",
824 | "stream"
825 | ],
826 | "support": {
827 | "issues": "https://github.com/reactphp/socket/issues",
828 | "source": "https://github.com/reactphp/socket/tree/v1.12.0"
829 | },
830 | "funding": [
831 | {
832 | "url": "https://github.com/WyriHaximus",
833 | "type": "github"
834 | },
835 | {
836 | "url": "https://github.com/clue",
837 | "type": "github"
838 | }
839 | ],
840 | "time": "2022-08-25T12:32:25+00:00"
841 | },
842 | {
843 | "name": "react/stream",
844 | "version": "v1.2.0",
845 | "source": {
846 | "type": "git",
847 | "url": "https://github.com/reactphp/stream.git",
848 | "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9"
849 | },
850 | "dist": {
851 | "type": "zip",
852 | "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9",
853 | "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9",
854 | "shasum": ""
855 | },
856 | "require": {
857 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
858 | "php": ">=5.3.8",
859 | "react/event-loop": "^1.2"
860 | },
861 | "require-dev": {
862 | "clue/stream-filter": "~1.2",
863 | "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
864 | },
865 | "type": "library",
866 | "autoload": {
867 | "psr-4": {
868 | "React\\Stream\\": "src"
869 | }
870 | },
871 | "notification-url": "https://packagist.org/downloads/",
872 | "license": [
873 | "MIT"
874 | ],
875 | "authors": [
876 | {
877 | "name": "Christian Lück",
878 | "email": "christian@clue.engineering",
879 | "homepage": "https://clue.engineering/"
880 | },
881 | {
882 | "name": "Cees-Jan Kiewiet",
883 | "email": "reactphp@ceesjankiewiet.nl",
884 | "homepage": "https://wyrihaximus.net/"
885 | },
886 | {
887 | "name": "Jan Sorgalla",
888 | "email": "jsorgalla@gmail.com",
889 | "homepage": "https://sorgalla.com/"
890 | },
891 | {
892 | "name": "Chris Boden",
893 | "email": "cboden@gmail.com",
894 | "homepage": "https://cboden.dev/"
895 | }
896 | ],
897 | "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
898 | "keywords": [
899 | "event-driven",
900 | "io",
901 | "non-blocking",
902 | "pipe",
903 | "reactphp",
904 | "readable",
905 | "stream",
906 | "writable"
907 | ],
908 | "support": {
909 | "issues": "https://github.com/reactphp/stream/issues",
910 | "source": "https://github.com/reactphp/stream/tree/v1.2.0"
911 | },
912 | "funding": [
913 | {
914 | "url": "https://github.com/WyriHaximus",
915 | "type": "github"
916 | },
917 | {
918 | "url": "https://github.com/clue",
919 | "type": "github"
920 | }
921 | ],
922 | "time": "2021-07-11T12:37:55+00:00"
923 | }
924 | ],
925 | "packages-dev": [],
926 | "aliases": [],
927 | "minimum-stability": "stable",
928 | "stability-flags": [],
929 | "prefer-stable": false,
930 | "prefer-lowest": false,
931 | "platform": {
932 | "php": ">=7.1",
933 | "ext-curl": "*"
934 | },
935 | "platform-dev": [],
936 | "plugin-api-version": "2.2.0"
937 | }
938 |
--------------------------------------------------------------------------------