├── FUNDING.yml
├── .gitignore
├── tests
├── TestCase.php
├── Unit
│ └── JoinTest.php
└── Feature
│ ├── OnOffCommandTest.php
│ ├── ClientTest.php
│ └── FdgtTest.php
├── src
├── Events
│ ├── Irc
│ │ ├── WelcomeEvent.php
│ │ ├── PingEvent.php
│ │ ├── MotdEvent.php
│ │ ├── NameReplyEvent.php
│ │ ├── TopicChangeEvent.php
│ │ ├── KickEvent.php
│ │ ├── JoinEvent.php
│ │ ├── PartEvent.php
│ │ └── PrivmsgEvent.php
│ ├── Inspector
│ │ ├── InspectorReadyEvent.php
│ │ └── InspectorEvent.php
│ ├── Twitch
│ │ ├── ModsEvent.php
│ │ ├── VipsEvent.php
│ │ ├── UnhostEvent.php
│ │ ├── NoticeEvent.php
│ │ ├── R9kModeEvent.php
│ │ ├── UserNoticeEvent.php
│ │ ├── EmoteOnlyModeEvent.php
│ │ ├── SubsOnlyModeEvent.php
│ │ ├── RoomStateEvent.php
│ │ ├── RaidEvent.php
│ │ ├── HostingEvent.php
│ │ ├── SlowModeEvent.php
│ │ ├── FollowersOnlyModeEvent.php
│ │ ├── AnonGiftPaidUpgradeEvent.php
│ │ ├── HostedEvent.php
│ │ ├── GiftPaidUpgradeEvent.php
│ │ ├── PrimePaidUpgradeEvent.php
│ │ ├── AnonSubMysteryGiftEvent.php
│ │ ├── RitualEvent.php
│ │ ├── CheerEvent.php
│ │ ├── MessageEvent.php
│ │ ├── SubEvent.php
│ │ ├── SubMysteryGiftEvent.php
│ │ ├── AnonSubGiftEvent.php
│ │ ├── ResubEvent.php
│ │ └── SubGiftEvent.php
│ ├── EventHandler.php
│ └── Event.php
├── Traits
│ ├── HasTagSignature.php
│ └── Irc.php
├── Messages
│ ├── IrcCommand.php
│ ├── MotdMessage.php
│ ├── PingMessage.php
│ ├── WelcomeMessage.php
│ ├── TopicChangeMessage.php
│ ├── NameReplyMessage.php
│ ├── JoinMessage.php
│ ├── PartMessage.php
│ ├── KickMessage.php
│ ├── HostTargetMessage.php
│ ├── RoomStateMessage.php
│ ├── IrcMessage.php
│ ├── PrivmsgMessage.php
│ ├── NoticeMessage.php
│ ├── IrcMessageParser.php
│ └── UserNoticeMessage.php
├── Plan.php
├── Exceptions
│ └── ParseException.php
├── Tags.php
├── Channel.php
├── ClientOptions.php
└── Client.php
├── composer.json
├── phpunit.xml
├── .github
└── workflows
│ └── tests.yml
├── LICENSE
└── README.md
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: ene
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /.idea
3 | composer.lock
4 | .phpunit.result.cache
5 | tmi.php
6 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | tags['id'] ?? null;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Messages/IrcCommand.php:
--------------------------------------------------------------------------------
1 | prime = $prime;
14 | $this->plan = $plan;
15 | $this->name = $name;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Exceptions/ParseException.php:
--------------------------------------------------------------------------------
1 | payload),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Messages/PingMessage.php:
--------------------------------------------------------------------------------
1 | write("PONG :$this->payload");
13 |
14 | return [
15 | new PingEvent(),
16 | ];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Events/Irc/PingEvent.php:
--------------------------------------------------------------------------------
1 | message = $message;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Events/Inspector/InspectorReadyEvent.php:
--------------------------------------------------------------------------------
1 | url = $url;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Events/Inspector/InspectorEvent.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Messages/WelcomeMessage.php:
--------------------------------------------------------------------------------
1 | getOptions()->getChannels() as $channel) {
13 | $client->join($channel);
14 | }
15 |
16 | return [
17 | new WelcomeEvent(),
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Events/Twitch/ModsEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->users = $users;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/VipsEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->users = $users;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/UnhostEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->viewers = $viewers;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Irc/NameReplyEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->names = $names;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/NoticeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->messageId = $messageId;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/R9kModeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->enabled = $enabled;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/UserNoticeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->messageId = $messageId;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/EmoteOnlyModeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->enabled = $enabled;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Twitch/SubsOnlyModeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
23 | $this->enabled = $enabled;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Unit/JoinTest.php:
--------------------------------------------------------------------------------
1 | parse('JOIN #test') as $message) {
17 | self::assertNotNull($message);
18 | }
19 | } catch (Throwable $throwable) {
20 | self::assertInstanceOf(ParseException::class, $throwable);
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/Events/Twitch/RoomStateEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
27 | $this->tags = $tags;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Events/Irc/TopicChangeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
26 | $this->topic = $topic;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghostzero/tmi",
3 | "description": "PHP Twitch Messaging Interface",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "René Preuß",
9 | "email": "rene@preuss.io"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": {
14 | "GhostZero\\Tmi\\": "src/"
15 | }
16 | },
17 | "autoload-dev": {
18 | "psr-4": {
19 | "Tests\\": "tests/"
20 | }
21 | },
22 | "require": {
23 | "php": "^7.4|^8.0",
24 | "react/socket": "^1.6",
25 | "ext-mbstring": "*",
26 | "ext-json": "*"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^9.3.3"
30 | },
31 | "minimum-stability": "dev",
32 | "prefer-stable": true
33 | }
34 |
--------------------------------------------------------------------------------
/src/Events/Twitch/RaidEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
28 | $this->user = $user;
29 | $this->viewers = $viewers;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Events/Twitch/HostingEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
28 | $this->target = $target;
29 | $this->viewers = $viewers;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | ./tests/Unit
9 |
10 |
11 | ./tests/Feature
12 |
13 |
14 |
15 |
16 | ./src
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/Events/Irc/KickEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
31 | $this->user = $user;
32 | $this->message = $message;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Events/Twitch/SlowModeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
28 | $this->enabled = $enabled;
29 | $this->minutes = $minutes;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Messages/TopicChangeMessage.php:
--------------------------------------------------------------------------------
1 | channel = strstr($this->commandSuffix, '#');
19 | $this->topic = $this->payload;
20 | }
21 |
22 | public function handle(Client $client, array $channels): array
23 | {
24 | $channel = $client->getChannel($this->channel);
25 | $channel->setTopic($this->topic);
26 |
27 | return [
28 | new TopicChangeEvent($channel, $this->topic),
29 | ];
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/Events/Twitch/FollowersOnlyModeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
28 | $this->enabled = $enabled;
29 | $this->minutes = $minutes;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Events/Twitch/AnonGiftPaidUpgradeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
32 | $this->user = $user;
33 | $this->tags = $tags;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Messages/NameReplyMessage.php:
--------------------------------------------------------------------------------
1 | channel = strstr($this->commandSuffix, '#');
19 | $this->names = explode(' ', $this->payload ?? '');
20 | }
21 |
22 | public function handle(Client $client, array $channels): array
23 | {
24 | $channel = $client->getChannel($this->channel);
25 | if (!empty($this->names)) {
26 | $channel->setUsers($this->names);
27 | }
28 |
29 | return [
30 | new NameReplyEvent($channel, $this->names),
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Events/Twitch/HostedEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
33 | $this->user = $user;
34 | $this->viewers = $viewers;
35 | $this->autoHost = $autoHost;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Feature/OnOffCommandTest.php:
--------------------------------------------------------------------------------
1 | ['debug' => true],
18 | 'connection' => [
19 | 'secure' => true,
20 | 'reconnect' => true,
21 | 'rejoin' => true,
22 | ],
23 | 'channels' => ['ghostzero']
24 | ]));
25 |
26 | $called = false;
27 |
28 | $client->connect(function () use (&$called) {
29 | $called = true; // mark this test as successful
30 | });
31 |
32 | sleep(3);
33 |
34 | self::assertTrue($called, '');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Feature/ClientTest.php:
--------------------------------------------------------------------------------
1 | ['debug' => true],
19 | 'connection' => [
20 | 'secure' => true,
21 | 'reconnect' => true,
22 | 'rejoin' => true,
23 | ],
24 | 'channels' => ['ghostzero']
25 | ]));
26 |
27 | $client->on(NameReplyEvent::class, function (NameReplyEvent $event) {
28 | $this->assertEquals('#ghostzero', $event->channel);
29 | $event->client->close();
30 | });
31 |
32 | $client->connect();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Feature/FdgtTest.php:
--------------------------------------------------------------------------------
1 | ['debug' => true],
21 | 'connection' => [
22 | 'secure' => true,
23 | 'server' => 'irc.fdgt.dev',
24 | ],
25 | 'channels' => ['ghostzero']
26 | ]));
27 |
28 | $client->on(WelcomeEvent::class, function () use ($client) {
29 | $this->assertTrue(true);
30 | $client->close();
31 | });
32 |
33 | $client->connect();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests:
7 |
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | php: [7.4, 8.0]
12 | composer-dependency: [prefer-stable, prefer-lowest]
13 |
14 | name: "PHP ${{ matrix.php }} - ${{ matrix.composer-dependency }}"
15 |
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Setup PHP
22 | uses: nanasess/setup-php@master
23 | with:
24 | php-version: ${{ matrix.php }}
25 |
26 | - name: Install dependencies
27 | run: composer update --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist --${{ matrix.composer-dependency }}
28 |
29 | - name: PHPUnit Tests
30 | run: vendor/bin/phpunit
31 | env:
32 | CLIENT_ID: ${{ secrets.TMI_IDENTITY_USERNAME }}
33 | CLIENT_SECRET: ${{ secrets.TMI_IDENTITY_PASSWORD }}
34 |
35 |
--------------------------------------------------------------------------------
/src/Events/Irc/JoinEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
37 | $this->tags = $tags;
38 | $this->user = $user;
39 | $this->message = $message;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Events/Irc/PartEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
37 | $this->tags = $tags;
38 | $this->user = $user;
39 | $this->message = $message;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Events/Twitch/GiftPaidUpgradeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
37 | $this->user = $user;
38 | $this->sender = $sender;
39 | $this->tags = $tags;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Messages/JoinMessage.php:
--------------------------------------------------------------------------------
1 | user = strstr($this->source, '!', true);
20 | $this->message = $message;
21 | }
22 |
23 | public function handle(Client $client, array $channels): array
24 | {
25 | if (array_key_exists($this->commandSuffix, $channels)) {
26 | $this->channel = $channels[$this->commandSuffix];
27 | } else {
28 | $this->channel = new Channel($this->commandSuffix);
29 | }
30 |
31 | return [
32 | new JoinEvent($this->channel, $this->tags, $this->user, $this->message),
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Messages/PartMessage.php:
--------------------------------------------------------------------------------
1 | user = strstr($this->source, '!', true);
20 | $this->message = $message;
21 | }
22 |
23 | public function handle(Client $client, array $channels): array
24 | {
25 | if (array_key_exists($this->commandSuffix, $channels)) {
26 | $this->channel = $channels[$this->commandSuffix];
27 | } else {
28 | $this->channel = new Channel($this->commandSuffix);
29 | }
30 |
31 | return [
32 | new PartEvent($this->channel, $this->tags, $this->user, $this->message),
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Events/Twitch/PrimePaidUpgradeEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
38 | $this->user = $user;
39 | $this->plan = $plan;
40 | $this->tags = $tags;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Events/EventHandler.php:
--------------------------------------------------------------------------------
1 | eventHandlers = [];
12 | }
13 |
14 | public function addHandler(string $event, callable $function): void
15 | {
16 | if (!array_key_exists($event, $this->eventHandlers)) {
17 | $this->eventHandlers[$event] = [];
18 | }
19 |
20 | $this->eventHandlers[$event][] = $function;
21 | }
22 |
23 | public function invoke(Event $event): void
24 | {
25 | $handlers = $this->eventHandlers['*'] ?? [];
26 | $this->invokeHandlers($handlers, $event);
27 | $handlers = $this->eventHandlers[get_class($event)] ?? [];
28 | $this->invokeHandlers($handlers, $event);
29 | }
30 |
31 | protected function invokeHandlers(array $handlers, Event $event): void
32 | {
33 | foreach ($handlers as $handler) {
34 | $handler($event);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Events/Twitch/AnonSubMysteryGiftEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
39 | $this->giftSubCount = $giftSubCount;
40 | $this->plan = $plan;
41 | $this->tags = $tags;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 René Preuß
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/Events/Twitch/RitualEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
42 | $this->ritual = $ritual;
43 | $this->user = $user;
44 | $this->tags = $tags;
45 | $this->message = $message;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Irc/PrivmsgEvent.php:
--------------------------------------------------------------------------------
1 | target = $target;
42 | $this->tags = $tags;
43 | $this->user = $user;
44 | $this->message = $message;
45 | $this->self = $self;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Twitch/CheerEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
42 | $this->tags = $tags;
43 | $this->user = $user;
44 | $this->message = $message;
45 | $this->self = $self;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Twitch/MessageEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
42 | $this->tags = $tags;
43 | $this->user = $user;
44 | $this->message = $message;
45 | $this->self = $self;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Twitch/SubEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
43 | $this->user = $user;
44 | $this->plan = $plan;
45 | $this->message = $message;
46 | $this->tags = $tags;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Events/Twitch/SubMysteryGiftEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
43 | $this->user = $user;
44 | $this->giftSubCount = $giftSubCount;
45 | $this->plan = $plan;
46 | $this->tags = $tags;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Messages/KickMessage.php:
--------------------------------------------------------------------------------
1 | target, $this->user] = explode(' ', $this->commandSuffix);
24 | $this->message = $this->payload;
25 | }
26 |
27 | public function handle(Client $client, array $channels): array
28 | {
29 | if (array_key_exists($this->target, $channels)) {
30 | $this->channel = $channels[$this->target];
31 | } else {
32 | $this->channel = new Channel($this->target);
33 | }
34 |
35 | if ($client->getOptions()->getNickname() === $this->user && $client->getOptions()->shouldAutoRejoin()) {
36 | $client->join($this->target);
37 | }
38 |
39 | return [
40 | new KickEvent($this->channel, $this->user, $this->message),
41 | ];
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Events/Twitch/AnonSubGiftEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
43 | $this->streakMonths = $streakMonths;
44 | $this->recipient = $recipient;
45 | $this->plan = $plan;
46 | $this->tags = $tags;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Messages/HostTargetMessage.php:
--------------------------------------------------------------------------------
1 | message = $message;
21 | }
22 |
23 | public function handle(Client $client, array $channels): array
24 | {
25 | if (array_key_exists($this->commandSuffix, $channels)) {
26 | $this->channel = $channels[$this->commandSuffix];
27 | } else {
28 | $this->channel = new Channel($this->commandSuffix);
29 | }
30 |
31 | $msgSplit = explode(' ', $this->payload);
32 | $viewers = (int)($msgSplit[1] ?? 0);
33 |
34 | if ($msgSplit[0] === '-') {
35 | return [
36 | new UnhostEvent($this->channel, $viewers),
37 | ];
38 | }
39 |
40 | return [
41 | new HostingEvent($this->channel, $msgSplit[0], $viewers),
42 | ];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Traits/Irc.php:
--------------------------------------------------------------------------------
1 | write("PRIVMSG {$channel} :{$message}");
17 | }
18 |
19 | public function whisper(string $user, string $message): void
20 | {
21 | $this->say('#tmi_inspector', sprintf('/w %s %s', $user, $message));
22 | }
23 |
24 | public function join(string $channel): void
25 | {
26 | $channel = Channel::sanitize($channel);
27 | $this->channels[$channel] = new Channel($channel, true);
28 | $this->write("JOIN {$channel}");
29 | }
30 |
31 | public function part(string $channel): void
32 | {
33 | $channel = Channel::sanitize($channel);
34 | unset($this->channels[$channel]);
35 | $this->write("PART {$channel}");
36 | }
37 |
38 | public function getChannels(): array
39 | {
40 | return $this->channels;
41 | }
42 |
43 | public function getChannel(string $channel): Channel
44 | {
45 | return $this->channels[$channel];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Twitch/ResubEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
48 | $this->user = $user;
49 | $this->streakMonths = $streakMonths;
50 | $this->message = $message;
51 | $this->tags = $tags;
52 | $this->plan = $plan;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Events/Event.php:
--------------------------------------------------------------------------------
1 | getProperties();
28 |
29 | foreach ($props as $prop) {
30 | if ($prop->getName() === 'client') {
31 | continue;
32 | }
33 | $data[$prop->getName()] = ['s' => $prop->isStatic(), 'v' => $prop->getValue($this)];
34 | }
35 |
36 | return $data;
37 | }
38 |
39 | public function __unserialize(array $data): void
40 | {
41 | $reflect = new ReflectionClass($this);
42 |
43 | foreach ($data as $name => $prop) {
44 | if ($prop['s']) {
45 | $reflect->getProperty($name)->setValue($prop['v']);
46 | } else {
47 | $reflect->getProperty($name)->setValue($this, $prop['v']);
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Events/Twitch/SubGiftEvent.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
48 | $this->user = $user;
49 | $this->streakMonths = $streakMonths;
50 | $this->recipient = $recipient;
51 | $this->plan = $plan;
52 | $this->tags = $tags;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Tags.php:
--------------------------------------------------------------------------------
1 | tags = $tags;
16 | }
17 |
18 | public static function parse(string $message): self
19 | {
20 | $result = [];
21 | $rawTags = explode(';', $message);
22 | foreach ($rawTags as $rawTag) {
23 | $data = explode('=', $rawTag);
24 | $result[$data[0]] = $data[1] ?? null;
25 | }
26 |
27 | return new self($result);
28 | }
29 |
30 | public function offsetSet($offset, $value): void
31 | {
32 | if (is_null($offset)) {
33 | $this->tags[] = $value;
34 | } else {
35 | $this->tags[$offset] = $value;
36 | }
37 | }
38 |
39 | public function offsetExists($offset): bool
40 | {
41 | return isset($this->tags[$offset]);
42 | }
43 |
44 | public function offsetUnset($offset): void
45 | {
46 | unset($this->tags[$offset]);
47 | }
48 |
49 | public function offsetGet($offset)
50 | {
51 | return $this->tags[$offset] ?? null;
52 | }
53 |
54 | public function getTags(): array
55 | {
56 | return $this->tags;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Channel.php:
--------------------------------------------------------------------------------
1 | channel = $channel;
18 | $this->persisted = $persisted;
19 | }
20 |
21 | public function getName(): string
22 | {
23 | return $this->channel;
24 | }
25 |
26 | public function getTopic(): string
27 | {
28 | return $this->topic;
29 | }
30 |
31 | public function setTopic(string $topic): void
32 | {
33 | $this->topic = $topic;
34 | }
35 |
36 | public function getUsers(): array
37 | {
38 | return $this->users;
39 | }
40 |
41 | public function setUsers(array $users): void
42 | {
43 | $this->users = $users;
44 | }
45 |
46 | public function isPersisted(): bool
47 | {
48 | return $this->persisted;
49 | }
50 |
51 | public function __toString()
52 | {
53 | return $this->getName();
54 | }
55 |
56 | public static function sanitize(string $channel): string
57 | {
58 | if ($channel[0] !== '#') {
59 | $channel = "#$channel";
60 | }
61 |
62 | return strtolower($channel);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Messages/RoomStateMessage.php:
--------------------------------------------------------------------------------
1 | commandSuffix, $channels)) {
19 | $this->channel = $channels[$this->commandSuffix];
20 | } else {
21 | $this->channel = new Channel($this->commandSuffix);
22 | }
23 |
24 | $events = [
25 | new RoomStateEvent($this->channel, $this->tags),
26 | ];
27 |
28 | if ($event = $this->getSpecificEvent()) {
29 | $events[] = $event;
30 | }
31 |
32 | return $events;
33 | }
34 |
35 | public function getSpecificEvent(): ?Event
36 | {
37 | $tags = $this->tags;
38 |
39 | if (array_key_exists('slow', $tags->getTags())) {
40 | if (is_bool($tags['slow']) && !$tags['slow']) {
41 | return new SlowModeEvent($this->channel, false);
42 | }
43 |
44 | $minutes = (int)$tags['slow'];
45 | return new SlowModeEvent($this->channel, true, $minutes);
46 | }
47 |
48 | if (array_key_exists('followers-only', $tags->getTags())) {
49 | if ($tags['followers-only'] === '-1') {
50 | return new FollowersOnlyModeEvent($this->channel, false);
51 | }
52 |
53 | $minutes = (int)$tags['followers-only'];
54 | return new FollowersOnlyModeEvent($this->channel, true, $minutes);
55 | }
56 |
57 | return null;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Messages/IrcMessage.php:
--------------------------------------------------------------------------------
1 | parse($command);
26 | }
27 |
28 | /**
29 | * Handles the message and returns a array of events to invoke.
30 | *
31 | * @param Client $client
32 | * @param array $channels
33 | * @return Event[]
34 | */
35 | public function handle(Client $client, array $channels): array
36 | {
37 | return [];
38 | }
39 |
40 | private function parse(string $command): void
41 | {
42 | $command = trim($command);
43 | $this->rawMessage = $command;
44 | $i = 0;
45 |
46 | if ($command[0] === ':') {
47 | $i = strpos($command, ' ');
48 | $this->source = substr($command, 1, $i - 1);
49 |
50 | $i++;
51 | }
52 |
53 | $j = strpos($command, ' ', $i);
54 | if ($j !== false) {
55 | $this->command = substr($command, $i, $j - $i);
56 | } else {
57 | $this->command = substr($command, $i);
58 |
59 | return;
60 | }
61 |
62 | $i = strpos($command, ':', $j);
63 | if ($i !== false) {
64 | if ($i !== $j + 1) {
65 | $this->commandSuffix = substr($command, $j + 1, $i - $j - 2);
66 | }
67 | $this->payload = substr($command, $i + 1);
68 | } else {
69 | $this->commandSuffix = substr($command, $j + 1);
70 | }
71 | }
72 |
73 | public function withTags(Tags $tags): self
74 | {
75 | $this->tags = $tags;
76 | return $this;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/ClientOptions.php:
--------------------------------------------------------------------------------
1 | options = $options;
12 |
13 | if (empty($this->options['identity']['username']) || empty($this->options['identity']['password'])) {
14 | $this->options['identity'] = [
15 | 'username' => $this->options['identity']['username'] ?? ('justinfan' . random_int(1000, 80000)),
16 | 'password' => 'SCHMOOPIIE'
17 | ];
18 | }
19 | }
20 |
21 | public function isDebug(): bool
22 | {
23 | return $this->options['options']['debug'] ?? false;
24 | }
25 |
26 | public function getExecutionTimeout(): float
27 | {
28 | return (float)($this->options['options']['execution_timeout'] ?? 1.5);
29 | }
30 |
31 | public function getIdentity(): array
32 | {
33 | return $this->options['identity'];
34 | }
35 |
36 | public function getChannels(): array
37 | {
38 | return $this->options['channels'] ?? [];
39 | }
40 |
41 | public function getNickname(): string
42 | {
43 | return $this->options['identity']['username'];
44 | }
45 |
46 | public function getServer(): string
47 | {
48 | return $this->options['connection']['server'] ?? 'irc.chat.twitch.tv';
49 | }
50 |
51 | public function shouldAutoRejoin(): bool
52 | {
53 | return $this->options['connection']['rejoin'] ?? true;
54 | }
55 |
56 | public function shouldReconnect(): bool
57 | {
58 | return $this->options['connection']['reconnect'] ?? true;
59 | }
60 |
61 | public function setShouldReconnect(bool $reconnect): void
62 | {
63 | $this->options['connection']['reconnect'] = $reconnect;
64 | }
65 |
66 | public function shouldConnectSecure(): bool
67 | {
68 | return $this->options['connection']['secure'] ?? true;
69 | }
70 |
71 | public function getNameserver(): string
72 | {
73 | return $this->options['connection']['nameserver'] ?? '1.1.1.1';
74 | }
75 |
76 | public function getType(): string
77 | {
78 | return $this->options['identity']['type'] ?? 'verified';
79 | }
80 |
81 | public function getReconnectDelay(): int
82 | {
83 | return $this->options['connection']['reconnect_delay'] ?? 3;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Twitch Messaging Interface
2 |
3 |
4 |
5 |
6 |
7 |
8 | ## Introduction
9 |
10 | Inspired by [tmi.js](https://github.com/tmijs/tmi.js) and [php-irc-client](https://github.com/jerodev/php-irc-client) this package is a full featured, high performance Twitch IRC client written in PHP 7.4.
11 |
12 | Also have a look at [ghostzero/tmi-cluster](https://github.com/ghostzero/tmi-cluster). TMI Cluster is a Laravel package that makes the PHP TMI client scalable.
13 |
14 | ## Features
15 |
16 | - Connecting to Twitch IRC with SSL
17 | - Generic IRC Commands
18 | - Supports Twitch IRC Tags (IRC v3)
19 | - Supports Twitch IRC Membership
20 | - Supports Twitch IRC Commands
21 |
22 | ## Official Documentation
23 |
24 | You can view our official documentation [here](https://tmiphp.com/docs/).
25 |
26 | ## Getting Started (w/o OAuth Token)
27 |
28 | ```php
29 | use GhostZero\Tmi\Client;
30 | use GhostZero\Tmi\ClientOptions;
31 | use GhostZero\Tmi\Events\Twitch\MessageEvent;
32 |
33 | $client = new Client(new ClientOptions([
34 | 'connection' => [
35 | 'secure' => true,
36 | 'reconnect' => true,
37 | 'rejoin' => true,
38 | ],
39 | 'channels' => ['ghostzero']
40 | ]));
41 |
42 | $client->on(MessageEvent::class, function (MessageEvent $e) {
43 | print "{$e->tags['display-name']}: {$e->message}";
44 | });
45 |
46 | $client->connect();
47 | ```
48 |
49 | ## Getting Started (w/ OAuth Token)
50 |
51 | ```php
52 | use GhostZero\Tmi\Client;
53 | use GhostZero\Tmi\ClientOptions;
54 | use GhostZero\Tmi\Events\Twitch\MessageEvent;
55 |
56 | $client = new Client(new ClientOptions([
57 | 'options' => ['debug' => true],
58 | 'connection' => [
59 | 'secure' => true,
60 | 'reconnect' => true,
61 | 'rejoin' => true,
62 | ],
63 | 'identity' => [
64 | 'username' => 'ghostzero',
65 | 'password' => 'oauth:...',
66 | ],
67 | 'channels' => ['ghostzero']
68 | ]));
69 |
70 | $client->on(MessageEvent::class, function (MessageEvent $e) use ($client) {
71 | if ($e->self) return;
72 |
73 | if (strtolower($e->message) === '!hello') {
74 | $client->say($e->channel->getName(), "@{$e->user}, heya!");
75 | }
76 | });
77 |
78 | $client->connect();
79 | ```
80 |
--------------------------------------------------------------------------------
/src/Messages/PrivmsgMessage.php:
--------------------------------------------------------------------------------
1 | user = strstr($this->source, '!', true);
28 | $this->target = $this->commandSuffix;
29 | $this->message = $this->payload;
30 | }
31 |
32 | public function handle(Client $client, array $channels): array
33 | {
34 | if (array_key_exists($this->target, $channels)) {
35 | $this->channel = $channels[$this->target];
36 | } else {
37 | $this->channel = new Channel($this->target);
38 | }
39 |
40 | $self = $client->getOptions()->getNickname() === $this->user;
41 |
42 | if ($this->user === 'tmi_inspector') {
43 | $payload = json_decode($this->message, false, 512, JSON_THROW_ON_ERROR);
44 | $events = [new InspectorEvent($payload)];
45 | if ($payload->event === 'ready') {
46 | $events[] = new InspectorReadyEvent($payload->url);
47 | }
48 |
49 | return $events;
50 | }
51 |
52 | if ($this->user === 'jtv') {
53 | $autohost = (bool)strpos($this->message, 'auto');
54 | if ((bool)strpos($this->message, 'hosting you for')) {
55 | $count = (int)explode(' ', substr($this->message, strpos($this->message, 'hosting you for')))[3];
56 | return [
57 | new HostedEvent($this->channel, $this->user, $count, $autohost)
58 | ];
59 | }
60 |
61 | if ((bool)strpos($this->message, 'hosting you')) {
62 | return [
63 | new HostedEvent($this->channel, $this->user, 0, $autohost),
64 | ];
65 | }
66 | } elseif ($this->target[0] === '#') {
67 | $events = [
68 | new MessageEvent($this->channel, $this->tags, $this->user, $this->message, $self)
69 | ];
70 |
71 | if ($this->tags['bits']) {
72 | $events[] = new CheerEvent($this->channel, $this->tags, $this->user, $this->message, $self);
73 | }
74 |
75 | return $events;
76 | }
77 |
78 | return [
79 | new PrivmsgEvent($this->target, $this->tags, $this->user, $this->message, $self)
80 | ];
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Messages/NoticeMessage.php:
--------------------------------------------------------------------------------
1 | message = $message;
36 | }
37 |
38 | public function handle(Client $client, array $channels): array
39 | {
40 | if (array_key_exists($this->commandSuffix, $channels)) {
41 | $this->channel = $channels[$this->commandSuffix];
42 | } else {
43 | $this->channel = new Channel($this->commandSuffix);
44 | }
45 |
46 | $msgId = $this->tags['msg-id'] ?? '';
47 | $events = [
48 | new NoticeEvent($this->channel, $msgId),
49 | ];
50 |
51 | if (($event = $this->getSpecificEvent($msgId))) {
52 | $events[] = $event;
53 | }
54 |
55 | return $events;
56 | }
57 |
58 | public function getSpecificEvent(string $msgId): ?Event
59 | {
60 | switch ($msgId) {
61 | case self::TAG_EMOTEONLY_OFF:
62 | return new EmoteOnlyModeEvent($this->channel, false);
63 | case self::TAG_EMOTEONLY_ON:
64 | return new EmoteOnlyModeEvent($this->channel, true);
65 | case self::TAG_NO_MODS:
66 | return new ModsEvent($this->channel, []);
67 | case self::TAG_NO_VIPS:
68 | return new VipsEvent($this->channel, []);
69 | case self::TAG_R9K_MODE_OFF:
70 | return new R9kModeEvent($this->channel, false);
71 | case self::TAG_R9K_MODE_ON:
72 | return new R9kModeEvent($this->channel, true);
73 | case self::TAG_ROOM_MODS:
74 | $mods = array_filter(explode(', ', strtolower(explode(': ', $this->message)[1])), static fn($n) => $n);
75 | return new ModsEvent($this->channel, $mods);
76 | case self::TAG_SUBMODE_OFF:
77 | return new SubsOnlyModeEvent($this->channel, false);
78 | case self::TAG_SUBMODE_ON:
79 | return new SubsOnlyModeEvent($this->channel, true);
80 | case self::TAG_VIPS:
81 | if (substr($this->message, -strlen($this->message)) === '.') {
82 | $this->message = substr($this->message, 0, -1);
83 | }
84 | $vips = array_filter(explode(', ', strtolower(explode(': ', $this->message)[1])), static fn($n) => $n);
85 |
86 | return new VipsEvent($this->channel, $vips);
87 | default:
88 | return null;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Messages/IrcMessageParser.php:
--------------------------------------------------------------------------------
1 | parseSingle($msg);
27 | }
28 | }
29 |
30 | /**
31 | * @param string $message
32 | * @return IrcMessage
33 | * @throw ParseException
34 | */
35 | public function parseSingle(string $message): IrcMessage
36 | {
37 | try {
38 | [$tags, $message] = $this->parseTags($message);
39 |
40 | switch ($this->getCommand($message)) {
41 | case 'KICK':
42 | $msg = new KickMessage($message);
43 | break;
44 |
45 | case 'PING':
46 | $msg = new PingMessage($message);
47 | break;
48 |
49 | case 'PRIVMSG':
50 | $msg = new PrivmsgMessage($message);
51 | break;
52 |
53 | case IrcCommand::RPL_WELCOME:
54 | $msg = new WelcomeMessage($message);
55 | break;
56 |
57 | case 'TOPIC':
58 | case IrcCommand::RPL_TOPIC:
59 | $msg = new TopicChangeMessage($message);
60 | break;
61 |
62 | case IrcCommand::RPL_NAMREPLY:
63 | $msg = new NameReplyMessage($message);
64 | break;
65 |
66 | case IrcCommand::RPL_MOTD:
67 | $msg = new MotdMessage($message);
68 | break;
69 | case 'USERNOTICE':
70 | $msg = new UserNoticeMessage($message);
71 | break;
72 | case 'NOTICE':
73 | $msg = new NoticeMessage($message);
74 | break;
75 | case 'ROOMSTATE':
76 | $msg = new RoomStateMessage($message);
77 | break;
78 | case 'HOSTTARGET':
79 | $msg = new HostTargetMessage($message);
80 | break;
81 | case 'JOIN':
82 | $msg = new JoinMessage($message);
83 | break;
84 | case 'PART':
85 | $msg = new PartMessage($message);
86 | break;
87 |
88 | default:
89 | $msg = new IrcMessage($message);
90 | break;
91 | }
92 |
93 | return $msg->withTags($tags);
94 | } catch (Throwable $throwable) {
95 | throw ParseException::fromParseSingle($message, $throwable);
96 | }
97 | }
98 |
99 | private function getCommand(string $message): string
100 | {
101 | if ($message[0] === ':') {
102 | $message = trim(strstr($message, ' '));
103 | }
104 |
105 | return strstr($message, ' ', true);
106 | }
107 |
108 | private function parseTags(string $message): array
109 | {
110 | if ($message[0] !== '@') {
111 | return [new Tags(), $message];
112 | }
113 |
114 | $data = explode(' ', $message);
115 |
116 | return [
117 | Tags::parse(ltrim($data[0], '@')),
118 | implode(' ', array_slice($data, 1)),
119 | ];
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/Messages/UserNoticeMessage.php:
--------------------------------------------------------------------------------
1 | message = $message;
33 | }
34 |
35 | public function handle(Client $client, array $channels): array
36 | {
37 | if (array_key_exists($this->commandSuffix, $channels)) {
38 | $this->channel = $channels[$this->commandSuffix];
39 | } else {
40 | $this->channel = new Channel($this->commandSuffix);
41 | }
42 |
43 | $msgId = $this->tags['msg-id'] ?? '';
44 |
45 | $events = [
46 | new Twitch\UserNoticeEvent($this->channel, $msgId)
47 | ];
48 |
49 | if ($event = $this->getArguments($msgId)) {
50 | $events[] = $event;
51 | }
52 |
53 | return $events;
54 | }
55 |
56 | public function getArguments(string $msgId): ?Event
57 | {
58 | $tags = $this->tags;
59 | $username = $tags['display-name'] ?? $tags['login'] ?? '';
60 | $subPlan = $tags['msg-param-sub-plan'] ?? '';
61 | $planName = $tags['msg-param-sub-plan-name'] ?? '';
62 | $prime = strpos($subPlan, 'Prime') !== false;
63 | $plan = new Plan($prime, $subPlan, $planName);
64 | $streakMonths = (int)($tags['msg-param-streak-months'] ?? 0);
65 | $recipient = $tags['msg-param-recipient-display-name'] ?? $tags['msg-param-recipient-user-name'] ?? '';
66 | $giftSubCount = (int)($tags['msg-param-mass-gift-count'] ?? 0);
67 | $sender = $tags['msg-param-sender-name'] ?? $tags['msg-param-sender-login'] ?? '';
68 | $raidedChannel = $tags['msg-param-displayName'] ?? $tags['msg-param-login'] ?? '';
69 | $viewers = (int)($tags['msg-param-viewerCount'] ?? 0);
70 | $ritual = $tags['msg-param-ritual-name'] ?? '';
71 | $message = $this->payload ?? '';
72 |
73 | switch ($msgId) {
74 | case self::TAG_ANONGIFTPAIDUPGRADE:
75 | return new Twitch\AnonGiftPaidUpgradeEvent($this->channel, $username, $tags);
76 | case self::TAG_ANONSUBGIFT:
77 | return new Twitch\AnonSubGiftEvent($this->channel, $streakMonths, $recipient, $plan, $tags);
78 | case self::TAG_ANONSUBMYSTERYGIFT:
79 | return new Twitch\AnonSubMysteryGiftEvent($this->channel, $giftSubCount, $plan, $tags);
80 | case self::TAG_GIFTPAIDUPGRADE:
81 | return new Twitch\GiftPaidUpgradeEvent($this->channel, $username, $sender, $tags);
82 | case self::TAG_PRIMEPAIDUPGRADE:
83 | return new Twitch\PrimePaidUpgradeEvent($this->channel, $username, $plan, $tags);
84 | case self::TAG_RAID:
85 | return new Twitch\RaidEvent($this->channel, $raidedChannel, $viewers);
86 | case self::TAG_RESUB:
87 | return new Twitch\ResubEvent($this->channel, $username, $streakMonths, $message, $tags, $plan);
88 | case self::TAG_RITUAL:
89 | return new Twitch\RitualEvent($this->channel, $ritual, $username, $tags, $message);
90 | case self::TAG_SUB:
91 | return new Twitch\SubEvent($this->channel, $username, $plan, $message, $tags);
92 | case self::TAG_SUBGIFT:
93 | return new Twitch\SubGiftEvent($this->channel, $username, $streakMonths, $recipient, $plan, $tags);
94 | case self::TAG_SUBMYSTERYGIFT:
95 | return new Twitch\SubMysteryGiftEvent($this->channel, $username, $giftSubCount, $plan, $tags);
96 | default:
97 | return null;
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Client.php:
--------------------------------------------------------------------------------
1 | options = $options;
39 | $this->loop = \React\EventLoop\Factory::create();
40 | $this->ircMessageParser = new IrcMessageParser();
41 | $this->eventHandler = new EventHandler();
42 | }
43 |
44 | public function connect(?callable $execute = null): void
45 | {
46 | $tcpConnector = new TcpConnector($this->loop);
47 | $dnsResolverFactory = new Factory();
48 | $dns = $dnsResolverFactory->createCached($this->options->getNameserver(), $this->loop);
49 | $dnsConnector = new DnsConnector($tcpConnector, $dns);
50 | $connectorPromise = $this->getConnectorPromise($dnsConnector);
51 |
52 | $connectorPromise->then(function (ConnectionInterface $connection) use ($execute) {
53 | $this->connection = $connection;
54 | $this->connected = true;
55 | $this->channels = [];
56 |
57 | $this->connection->on('data', function ($data) {
58 | foreach (explode("\n", $data) as $message) {
59 | if (empty(trim($message))) {
60 | continue;
61 | }
62 |
63 | try {
64 | $this->handleIrcMessage($this->ircMessageParser->parseSingle($message));
65 | } catch (ParseException $exception) {
66 | $this->debug($exception->getMessage());
67 | }
68 | }
69 | });
70 |
71 | $this->connection->on('close', function () use ($execute) {
72 | $this->connected = false;
73 |
74 | if (is_null($execute)) {
75 | $this->reconnect('Connection closed by Twitch.');
76 | }
77 | });
78 |
79 | $this->connection->on('end', function () {
80 | $this->connection->close();
81 | });
82 |
83 | // login & request all twitch Kappabilities
84 | $identity = $this->options->getIdentity();
85 | $this->write("PASS {$identity['password']}");
86 | $this->write("NICK {$identity['username']}");
87 | $this->write('CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands');
88 |
89 | if (!is_null($execute)) {
90 | $this->loop->addTimer($this->options->getExecutionTimeout(), fn () => $this->close());
91 |
92 | $execute();
93 | }
94 | }, fn($error) => $this->reconnect($error));
95 |
96 | $this->loop->run();
97 | }
98 |
99 | public function close(): void
100 | {
101 | $this->options->setShouldReconnect(false);
102 |
103 | if ($this->isConnected()) {
104 | $this->connection->close();
105 | $this->loop->stop();
106 | }
107 | }
108 |
109 | public function write(string $rawCommand): void
110 | {
111 | if (!$this->isConnected()) {
112 | throw new RuntimeException('No open connection was found to write commands to.');
113 | }
114 |
115 | // Make sure the command ends in a newline character
116 | if (mb_substr($rawCommand, -1) !== "\n") {
117 | $rawCommand .= "\n";
118 | }
119 |
120 | $this->connection->write($rawCommand);
121 | }
122 |
123 | public function isConnected(): bool
124 | {
125 | return isset($this->connection) && $this->connected;
126 | }
127 |
128 | private function handleIrcMessage(IrcMessage $message): void
129 | {
130 | $this->debug($message->rawMessage);
131 |
132 | $events = $message->handle($this, $this->channels);
133 |
134 | foreach ($events as $event) {
135 | $event->client = $this; // attach client to event
136 | $this->eventHandler->invoke($event);
137 | }
138 | }
139 |
140 | public function getLoop(): LoopInterface
141 | {
142 | return $this->loop;
143 | }
144 |
145 | public function getOptions(): ClientOptions
146 | {
147 | return $this->options;
148 | }
149 |
150 | public function getEventHandler(): EventHandler
151 | {
152 | return $this->eventHandler;
153 | }
154 |
155 | public function any(callable $closure): self
156 | {
157 | return $this->on('*', $closure);
158 | }
159 |
160 | public function on(string $event, callable $closure): self
161 | {
162 | $this->eventHandler->addHandler($event, $closure);
163 |
164 | return $this;
165 | }
166 |
167 | private function getConnectorPromise(DnsConnector $dnsConnector): Promise
168 | {
169 | if ($this->options->shouldConnectSecure()) {
170 | return (new SecureConnector($dnsConnector, $this->loop))
171 | ->connect(sprintf('%s:6697', $this->options->getServer()));
172 | }
173 |
174 | return $dnsConnector->connect(sprintf('%s:6667', $this->options->getServer()));
175 | }
176 |
177 | private function reconnect($error): void
178 | {
179 | if ($this->options->shouldReconnect()) {
180 | $seconds = $this->options->getReconnectDelay();
181 | $this->debug("Initialize reconnect in {$seconds} seconds... Error: " . $error);
182 | sleep($seconds);
183 | $this->connect();
184 | }
185 | }
186 |
187 | private function debug(string $message): void
188 | {
189 | if ($this->options->isDebug()) {
190 | print $message . PHP_EOL;
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------