├── 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 | Total Downloads 4 | Latest Stable Version 5 | License 6 | Discord 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 | --------------------------------------------------------------------------------