├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE.txt ├── Migrations ├── Version20220316094231.php ├── Version20240119003015.php ├── Version20240120061736.php ├── Version20240121020050.php ├── Version20240121033728.php ├── Version20240121075114.php ├── Version20240130061517.php ├── Version20240131035656.php ├── Version20240201012155.php ├── Version20240201172831.php └── Version20240204093244.php ├── README.md ├── admin-cli.php ├── artbot.php ├── artbot_rest_server.php ├── artbot_scripts ├── art-common.php ├── artfart.php ├── bashorg.php ├── drawing.php ├── farts.xml ├── help.php ├── quotes.php └── urlimg.php ├── artconfig.example.yaml ├── bootstrap.php ├── cli-config.php ├── cli_cmds ├── bot_add.php ├── bot_addchannel.php ├── bot_del.php ├── bot_delchannel.php ├── bot_list.php ├── bot_set.php ├── ignore_add.php ├── ignore_addnetwork.php ├── ignore_del.php ├── ignore_list.php ├── ignore_test.php ├── network_add.php ├── network_del.php ├── network_list.php ├── network_set.php ├── server_add.php ├── server_del.php ├── server_set.php └── showdb.php ├── composer.json ├── config.example.yaml ├── doctrine-cli.php ├── entities ├── Bot.php ├── Channel.php ├── Ignore.php ├── IgnoreRepository.php ├── Network.php └── Server.php ├── import_old_db ├── aliases.php ├── reminders.php ├── seen.php ├── tells.php └── weather.php ├── library ├── Channels.php ├── Duration.inc ├── Irc │ ├── Client.php │ ├── Consts.php │ ├── EventEmitter.php │ ├── Exception.php │ └── Message.php ├── Nicks.php ├── async_get_contents.php └── draw │ ├── Canvas.php │ ├── Color.php │ └── Pixel.php ├── lolbot.php ├── migrations.yml ├── multiartbot.php ├── multiartconfig.example.yaml ├── phpstan.neon ├── psalm.xml └── scripts ├── alias ├── alias.php └── entities │ └── alias.php ├── bing └── bing.php ├── bomb_game ├── bomb.php └── bomb_game.php ├── brave └── brave.php ├── codesand ├── codesand.php └── config.example.yaml ├── durendaltv └── durendaltv.php ├── github └── github.php ├── help └── help.php ├── insult └── insult.php ├── invidious └── invidious.php ├── lastfm ├── entities │ └── lastfm.php └── lastfm.php ├── linktitles ├── UrlEvent.php ├── cli_cmds │ ├── hostignore_add.php │ ├── hostignore_del.php │ ├── hostignore_list.php │ ├── hostignore_test.php │ ├── ignore_add.php │ ├── ignore_del.php │ ├── ignore_list.php │ └── ignore_test.php ├── entities │ ├── hostignore.php │ ├── ignore.php │ └── ignore_type.php └── linktitles.php ├── mal └── mal.php ├── notifier ├── notifier.php └── notifier_keys.example.yaml ├── owncast └── owncast.php ├── reddit └── reddit.php ├── remindme ├── entities │ └── reminder.php └── remindme.php ├── script_base.php ├── seen ├── entities │ └── seen.php └── seen.php ├── stocks ├── quote.php ├── stocks.php └── symbol.php ├── tell ├── entities │ └── tell.php └── tell.php ├── tiktok └── tiktok.php ├── tools └── tools.php ├── translate └── translate.php ├── twitter └── twitter.php ├── urbandict └── urbandict.php ├── user ├── Access.php └── user.php ├── weather ├── entities │ └── location.php └── weather.php ├── wiki └── wiki.php ├── wolfram └── wolfram.php ├── yoda ├── yoda-og.png ├── yoda.php └── yoda.png ├── youtube └── youtube.php └── zyzz ├── zyzz.php └── zyzz.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Specify filepatterns you want git to ignore. 2 | .idea 3 | ircart 4 | vendor 5 | config.yaml 6 | artconfig.yaml 7 | multiartconfig.yaml 8 | composer.lock 9 | scripts/notifier/notifier_keys.yaml 10 | ignores.txt 11 | composer-dev.lock 12 | /composer.phar 13 | /.phplint-cache 14 | *.db 15 | 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Listen for Xdebug", 9 | "type": "php", 10 | "request": "launch", 11 | "port": 9003 12 | }, 13 | { 14 | "name": "Launch artbot.php", 15 | "type": "php", 16 | "request": "launch", 17 | "program": "${workspaceFolder}/artbot.php", 18 | "cwd": "${workspaceFolder}", 19 | "port": 0, 20 | "runtimeArgs": [ 21 | "-dxdebug.start_with_request=yes" 22 | ], 23 | "env": { 24 | "XDEBUG_MODE": "debug,develop", 25 | "XDEBUG_CONFIG": "client_port=${port}" 26 | } 27 | }, 28 | { 29 | "name": "Launch lolbot.php", 30 | "type": "php", 31 | "request": "launch", 32 | "program": "${workspaceFolder}/lolbot.php", 33 | "cwd": "${workspaceFolder}", 34 | "port": 0, 35 | "runtimeArgs": [ 36 | "-dxdebug.start_with_request=yes" 37 | ], 38 | "env": { 39 | "XDEBUG_MODE": "debug,develop", 40 | "XDEBUG_CONFIG": "client_port=${port}" 41 | } 42 | }, 43 | { 44 | "name": "Launch Built-in web server", 45 | "type": "php", 46 | "request": "launch", 47 | "runtimeArgs": [ 48 | "-dxdebug.mode=debug", 49 | "-dxdebug.start_with_request=yes", 50 | "-S", 51 | "localhost:0" 52 | ], 53 | "program": "", 54 | "cwd": "${workspaceRoot}", 55 | "port": 9003, 56 | "serverReadyAction": { 57 | "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", 58 | "uriFormat": "http://localhost:%s", 59 | "action": "openExternally" 60 | } 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "php.suggest.basic": false 3 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2020 knivey@botops.net 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Migrations/Version20220316094231.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $Ignores = $newSchema->createTable("Ignores"); 28 | $Ignores->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $Ignores->setPrimaryKey(["id"]); 30 | $Ignores->addColumn("hostmask", Types::STRING)->setNotnull(true)->setLength(512); 31 | $Ignores->addColumn("reason", Types::STRING)->setNotnull(false)->setDefault(null)->setLength(512); 32 | $Ignores->addColumn("created", Types::DATETIME_IMMUTABLE); 33 | 34 | $Bots = $newSchema->createTable("Bots"); 35 | $Bots->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 36 | $Bots->setPrimaryKey(["id"]); 37 | $Bots->addColumn("name", Types::STRING)->setLength(512); 38 | $Bots->addColumn("created", Types::DATETIME_IMMUTABLE); 39 | $Bots->addColumn("network_id", Types::INTEGER); 40 | $Bots->addUniqueIndex(["name", "network_id"]); 41 | 42 | $Networks = $newSchema->createTable("Networks"); 43 | $Networks->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 44 | $Networks->setPrimaryKey(["id"]); 45 | $Networks->addColumn("name", Types::STRING)->setLength(512); 46 | $Networks->addUniqueIndex(["name"]); 47 | $Networks->addColumn("created", Types::DATETIME_IMMUTABLE); 48 | 49 | $Bots->addForeignKeyConstraint($Networks, ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 50 | 51 | $Ignore_Network = $newSchema->createTable("Ignore_Network"); 52 | $Ignore_Network->addColumn("ignore_id", Types::INTEGER)->setNotnull(true); 53 | $Ignore_Network->addColumn("network_id", Types::INTEGER)->setNotnull(true); 54 | $Ignore_Network->setPrimaryKey(["ignore_id", "network_id"]); 55 | 56 | $Ignore_Network->addForeignKeyConstraint($Networks, ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 57 | $Ignore_Network->addForeignKeyConstraint($Ignores, ["ignore_id"], ["id"], ["onDelete" => "CASCADE"]); 58 | 59 | $diff = $comp->compareSchemas($schema, $newSchema); 60 | 61 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 62 | $this->addSql($sql); 63 | } 64 | 65 | public function down(Schema $schema): void 66 | { 67 | $newSchemaManager = $this->connection->createSchemaManager(); 68 | $comp = $newSchemaManager->createComparator(); 69 | $newSchema = clone $schema; 70 | 71 | $newSchema->dropTable("Ignore_Network"); 72 | $newSchema->dropTable("Ignores"); 73 | $newSchema->dropTable("Bots"); 74 | $newSchema->dropTable("Networks"); 75 | 76 | $diff = $comp->compareSchemas($schema, $newSchema); 77 | 78 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 79 | $this->addSql($sql); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Migrations/Version20240119003015.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $linktitles_ignores = $newSchema->createTable("linktitles_ignores"); 28 | $linktitles_ignores->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $linktitles_ignores->setPrimaryKey(["id"]); 30 | $linktitles_ignores->addColumn("regex", Types::STRING)->setNotnull(true)->setLength(512); 31 | $linktitles_ignores->addColumn("type", Types::INTEGER)->setNotnull(true); 32 | $linktitles_ignores->addColumn("created", Types::DATETIME_IMMUTABLE); 33 | $linktitles_ignores->addColumn("network_id", Types::INTEGER)->setNotnull(false); 34 | $linktitles_ignores->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 35 | $linktitles_ignores->addColumn("bot_id", Types::INTEGER)->setNotnull(false); 36 | $linktitles_ignores->addForeignKeyConstraint("Bots", ["bot_id"], ["id"], ["onDelete" => "CASCADE"]); 37 | 38 | $linktitles_hostignores = $newSchema->createTable("linktitles_hostignores"); 39 | $linktitles_hostignores->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 40 | $linktitles_hostignores->setPrimaryKey(["id"]); 41 | $linktitles_hostignores->addColumn("hostmask", Types::STRING)->setNotnull(true)->setLength(512); 42 | $linktitles_hostignores->addColumn("type", Types::INTEGER)->setNotnull(true); 43 | $linktitles_hostignores->addColumn("created", Types::DATETIME_IMMUTABLE); 44 | $linktitles_hostignores->addColumn("network_id", Types::INTEGER)->setNotnull(false);; 45 | $linktitles_hostignores->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 46 | $linktitles_hostignores->addColumn("bot_id", Types::INTEGER)->setNotnull(false);; 47 | $linktitles_hostignores->addForeignKeyConstraint("Bots", ["bot_id"], ["id"], ["onDelete" => "CASCADE"]); 48 | 49 | $diff = $comp->compareSchemas($schema, $newSchema); 50 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 51 | $this->addSql($sql); 52 | } 53 | 54 | public function down(Schema $schema): void 55 | { 56 | $newSchemaManager = $this->connection->createSchemaManager(); 57 | $comp = $newSchemaManager->createComparator(); 58 | $newSchema = clone $schema; 59 | 60 | $newSchema->dropTable("linktitles_ignores"); 61 | $newSchema->dropTable("linktitles_hostignores"); 62 | 63 | $diff = $comp->compareSchemas($schema, $newSchema); 64 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 65 | $this->addSql($sql); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Migrations/Version20240120061736.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $location = $newSchema->createTable("weather_locations"); 28 | $location->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $location->setPrimaryKey(["id"]); 30 | $location->addColumn("si", Types::BOOLEAN)->setNotnull(true); 31 | $location->addColumn("name", Types::TEXT); 32 | $location->addColumn("lat", Types::STRING); 33 | $location->addColumn("long", Types::STRING); 34 | $location->addColumn("nick", Types::STRING); 35 | $location->addColumn("network_id", Types::INTEGER); 36 | $location->addUniqueConstraint(["nick", "network_id"]); 37 | $location->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 38 | 39 | $diff = $comp->compareSchemas($schema, $newSchema); 40 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 41 | $this->addSql($sql); 42 | } 43 | 44 | public function down(Schema $schema): void 45 | { 46 | $newSchemaManager = $this->connection->createSchemaManager(); 47 | $comp = $newSchemaManager->createComparator(); 48 | $newSchema = clone $schema; 49 | 50 | $newSchema->dropTable("weather_locations"); 51 | 52 | $diff = $comp->compareSchemas($schema, $newSchema); 53 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 54 | $this->addSql($sql); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Migrations/Version20240121020050.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $location = $newSchema->createTable("lastfm_users"); 28 | $location->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $location->setPrimaryKey(["id"]); 30 | $location->addColumn("lastfmUser", Types::TEXT); 31 | $location->addColumn("nick", Types::TEXT);; 32 | $location->addColumn("network_id", Types::INTEGER); 33 | $location->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 34 | 35 | $diff = $comp->compareSchemas($schema, $newSchema); 36 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 37 | $this->addSql($sql); 38 | } 39 | 40 | public function down(Schema $schema): void 41 | { 42 | $newSchemaManager = $this->connection->createSchemaManager(); 43 | $comp = $newSchemaManager->createComparator(); 44 | $newSchema = clone $schema; 45 | 46 | $newSchema->dropTable("lastfm_users"); 47 | 48 | $diff = $comp->compareSchemas($schema, $newSchema); 49 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 50 | $this->addSql($sql); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Migrations/Version20240121033728.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $location = $newSchema->createTable("alias_aliases"); 28 | $location->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $location->setPrimaryKey(["id"]); 30 | $location->addColumn("name", Types::TEXT); 31 | $location->addColumn("nameLowered", Types::TEXT); 32 | $location->addColumn("value", Types::TEXT); 33 | $location->addColumn("chan", Types::TEXT); 34 | $location->addColumn("chanLowered", Types::TEXT); 35 | $location->addColumn("fullhost", Types::TEXT); 36 | $location->addColumn("act", Types::BOOLEAN); 37 | $location->addColumn("cmd", Types::TEXT)->setNotnull(false); 38 | $location->addColumn("network_id", Types::INTEGER); 39 | $location->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 40 | 41 | $diff = $comp->compareSchemas($schema, $newSchema); 42 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 43 | $this->addSql($sql); 44 | } 45 | 46 | public function down(Schema $schema): void 47 | { 48 | $newSchemaManager = $this->connection->createSchemaManager(); 49 | $comp = $newSchemaManager->createComparator(); 50 | $newSchema = clone $schema; 51 | 52 | $newSchema->dropTable("alias_aliases"); 53 | 54 | $diff = $comp->compareSchemas($schema, $newSchema); 55 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 56 | $this->addSql($sql); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Migrations/Version20240121075114.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $bots = $newSchema->getTable("Bots"); 28 | $bots->addColumn("trigger", Types::TEXT)->setNotnull(false); 29 | $bots->addColumn("trigger_re", Types::TEXT)->setNotnull(false); 30 | $bots->addColumn("bindIp", Types::TEXT)->setDefault("0"); 31 | $bots->addColumn("onConnect", Types::TEXT)->setDefault(""); 32 | 33 | $servers = $newSchema->createTable("Servers"); 34 | $servers->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 35 | $servers->setPrimaryKey(["id"]); 36 | $servers->addColumn("address", Types::TEXT); 37 | $servers->addColumn("port", Types::INTEGER); 38 | $servers->addColumn("ssl", Types::BOOLEAN); 39 | $servers->addColumn("throttle", Types::BOOLEAN); 40 | $servers->addColumn("network_id", Types::INTEGER); 41 | $servers->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 42 | 43 | $channels = $newSchema->createTable("Channels"); 44 | $channels->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 45 | $channels->setPrimaryKey(["id"]); 46 | $channels->addColumn("name", Types::TEXT); 47 | $channels->addColumn("bot_id", Types::INTEGER); 48 | $channels->addForeignKeyConstraint("Bots", ["bot_id"], ["id"], ["onDelete" => "CASCADE"]); 49 | 50 | $diff = $comp->compareSchemas($schema, $newSchema); 51 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 52 | $this->addSql($sql); 53 | } 54 | 55 | public function down(Schema $schema): void 56 | { 57 | $newSchemaManager = $this->connection->createSchemaManager(); 58 | $comp = $newSchemaManager->createComparator(); 59 | $newSchema = clone $schema; 60 | 61 | $bots = $newSchema->getTable("Bots"); 62 | $bots->dropColumn("trigger") 63 | ->dropColumn("trigger_re") 64 | ->dropColumn("bindIp") 65 | ->dropColumn("onConnect"); 66 | 67 | $newSchema->dropTable("Servers"); 68 | $newSchema->dropTable("Channels"); 69 | 70 | $diff = $comp->compareSchemas($schema, $newSchema); 71 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 72 | $this->addSql($sql); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Migrations/Version20240130061517.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $reminders = $newSchema->createTable("remindme_reminders"); 28 | $reminders->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $reminders->setPrimaryKey(["id"]); 30 | $reminders->addColumn("nick", Types::TEXT); 31 | $reminders->addColumn("chan", Types::TEXT); 32 | $reminders->addColumn("at", Types::INTEGER); 33 | $reminders->addColumn("sent", Types::BOOLEAN); 34 | $reminders->addColumn("msg", Types::TEXT); 35 | $reminders->addColumn("network_id", Types::INTEGER); 36 | $reminders->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 37 | 38 | $diff = $comp->compareSchemas($schema, $newSchema); 39 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 40 | $this->addSql($sql); 41 | } 42 | 43 | public function down(Schema $schema): void 44 | { 45 | $newSchemaManager = $this->connection->createSchemaManager(); 46 | $comp = $newSchemaManager->createComparator(); 47 | $newSchema = clone $schema; 48 | 49 | $newSchema->dropTable("remindme_reminders"); 50 | 51 | $diff = $comp->compareSchemas($schema, $newSchema); 52 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 53 | $this->addSql($sql); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Migrations/Version20240131035656.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $tells = $newSchema->createTable("tell_tells"); 28 | $tells->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $tells->setPrimaryKey(["id"]); 30 | $tells->addColumn("created", Types::DATETIME_MUTABLE); 31 | $tells->addColumn("sender", Types::TEXT); 32 | $tells->addColumn("msg", Types::TEXT); 33 | $tells->addColumn("target", Types::TEXT); 34 | $tells->addColumn("sent", Types::BOOLEAN); 35 | $tells->addColumn("chan", Types::TEXT); 36 | $tells->addColumn("network_id", Types::INTEGER); 37 | $tells->addColumn("global", Types::BOOLEAN); 38 | $tells->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 39 | 40 | $diff = $comp->compareSchemas($schema, $newSchema); 41 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 42 | $this->addSql($sql); 43 | } 44 | 45 | public function down(Schema $schema): void 46 | { 47 | $newSchemaManager = $this->connection->createSchemaManager(); 48 | $comp = $newSchemaManager->createComparator(); 49 | $newSchema = clone $schema; 50 | 51 | $newSchema->dropTable("tell_tells"); 52 | 53 | $diff = $comp->compareSchemas($schema, $newSchema); 54 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 55 | $this->addSql($sql); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Migrations/Version20240201012155.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $tells = $newSchema->createTable("seen_seens"); 28 | $tells->addColumn("id", Types::INTEGER)->setNotnull(true)->setAutoincrement(true); 29 | $tells->setPrimaryKey(["id"]); 30 | $tells->addColumn("nick", Types::TEXT); 31 | $tells->addColumn("orig_nick", Types::TEXT); 32 | $tells->addColumn("chan", Types::TEXT); 33 | $tells->addColumn("text", Types::BINARY); 34 | $tells->addColumn("action", Types::TEXT); 35 | $tells->addColumn("time", Types::DATETIME_MUTABLE); 36 | $tells->addColumn("network_id", Types::INTEGER); 37 | $tells->addForeignKeyConstraint("Networks", ["network_id"], ["id"], ["onDelete" => "CASCADE"]); 38 | 39 | $diff = $comp->compareSchemas($schema, $newSchema); 40 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 41 | $this->addSql($sql); 42 | } 43 | 44 | public function down(Schema $schema): void 45 | { 46 | $newSchemaManager = $this->connection->createSchemaManager(); 47 | $comp = $newSchemaManager->createComparator(); 48 | $newSchema = clone $schema; 49 | 50 | $newSchema->dropTable("seen_seens"); 51 | 52 | $diff = $comp->compareSchemas($schema, $newSchema); 53 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 54 | $this->addSql($sql); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Migrations/Version20240201172831.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 24 | $comp = $newSchemaManager->createComparator(); 25 | $newSchema = clone $schema; 26 | 27 | $servers = $newSchema->getTable("Servers"); 28 | $servers->addColumn("password", Types::TEXT)->setNotnull(false); 29 | 30 | $bots = $newSchema->getTable("Bots"); 31 | $bots->addColumn("sasl_user", Types::TEXT)->setNotnull(false); 32 | $bots->addColumn("sasl_pass", Types::TEXT)->setNotnull(false); 33 | 34 | 35 | $diff = $comp->compareSchemas($schema, $newSchema); 36 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 37 | $this->addSql($sql); 38 | } 39 | 40 | public function down(Schema $schema): void 41 | { 42 | $newSchemaManager = $this->connection->createSchemaManager(); 43 | $comp = $newSchemaManager->createComparator(); 44 | $newSchema = clone $schema; 45 | 46 | $servers = $newSchema->getTable("Servers"); 47 | $servers->dropColumn("password"); 48 | 49 | $bots = $newSchema->getTable("Bots"); 50 | $bots->dropColumn("sasl_user"); 51 | $bots->dropColumn("sasl_pass"); 52 | 53 | $diff = $comp->compareSchemas($schema, $newSchema); 54 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 55 | $this->addSql($sql); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Migrations/Version20240204093244.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 27 | $comp = $newSchemaManager->createComparator(); 28 | $newSchema = clone $schema; 29 | 30 | $t = $newSchema->getTable("remindme_reminders"); 31 | $t->getColumn("at")->setType(new BigIntType()); 32 | 33 | $diff = $comp->compareSchemas($schema, $newSchema); 34 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 35 | $this->addSql($sql); 36 | } 37 | 38 | public function down(Schema $schema): void 39 | { 40 | $newSchemaManager = $this->connection->createSchemaManager(); 41 | $comp = $newSchemaManager->createComparator(); 42 | $newSchema = clone $schema; 43 | 44 | $t = $newSchema->getTable("remindme_reminders"); 45 | $t->getColumn("at")->setType(new IntegerType()); 46 | 47 | $diff = $comp->compareSchemas($schema, $newSchema); 48 | foreach ($this->platform->getAlterSchemaSQL($diff) as $sql) 49 | $this->addSql($sql); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lolbot 2 | This is just a quick bot I put together for friends in IRC, nothing serious. 3 | 4 | It's using the IRC library found here https://github.com/TorbenKoehn/php-irc 5 | which I modified a little to use Amphp. 6 | 7 | ## Running the bot 8 | I just run the bot inside a tmux session left open. 9 | 10 | To setup you need to edit config.yaml, artconfig.yaml or multiartconfig.yaml depending on what kind of bot you will run. 11 | There are many API keys that need to be obtained by you and placed in the config file, if you leave them commented out then the commands or functionality that need them will be disabled. 12 | 13 | For the normal sopel-like channel bot 14 | ``` 15 | php lolbot.php 16 | ``` 17 | 18 | For a artbot that uses 1 irc connection 19 | ``` 20 | php artbot.php 21 | ``` 22 | 23 | For an artbot that uses multiple irc connections (for slow networks) 24 | ``` 25 | php multiartbot.php 26 | ``` 27 | 28 | 29 | For the youtube thumbnails support and the @img command you need to download and compile https://github.com/knivey/p2u then set the path to it in your config.yaml 30 | ``` 31 | p2u: "/path/to/p2u" 32 | ``` 33 | 34 | For a2m you need to setup https://github.com/tat3r/a2m and edit the appropriate config setting for that also. 35 | 36 | 37 | ## Ignores 38 | 39 | Currently, all bots read from ignores.txt to ignore commands from hostmasks. It's a good idea to put other bots in the ignores to prevent them from endlessly triggering each other. 40 | 41 | The file is just one hostmask per line. 42 | 43 | You can edit the file while bots are running, They will reread it without needing to be restarted. 44 | 45 | ## Additional configuration 46 | The notifier system has a file in ```scripts/notifier/notifier_keys.yaml``` 47 | 48 | If you plan to use external tools that send notifications for the bot to display in channels you will need to add keys to that. -------------------------------------------------------------------------------- /admin-cli.php: -------------------------------------------------------------------------------- 1 | addCommands(array( 16 | new Command\DumpSchemaCommand($dependencyFactory), 17 | new Command\ExecuteCommand($dependencyFactory), 18 | new Command\GenerateCommand($dependencyFactory), 19 | new Command\LatestCommand($dependencyFactory), 20 | new Command\ListCommand($dependencyFactory), 21 | new Command\MigrateCommand($dependencyFactory), 22 | new Command\RollupCommand($dependencyFactory), 23 | new Command\StatusCommand($dependencyFactory), 24 | new Command\SyncMetadataCommand($dependencyFactory), 25 | new Command\VersionCommand($dependencyFactory), 26 | )); 27 | 28 | $application->add(new cli_cmds\ignore_add()); 29 | $application->add(new cli_cmds\ignore_del()); 30 | $application->add(new cli_cmds\ignore_list()); 31 | $application->add(new cli_cmds\ignore_addnetwork()); 32 | $application->add(new cli_cmds\ignore_test()); 33 | 34 | $application->add(new cli_cmds\bot_add()); 35 | $application->add(new cli_cmds\bot_del()); 36 | $application->add(new cli_cmds\bot_list()); 37 | $application->add(new cli_cmds\bot_set()); 38 | $application->add(new cli_cmds\bot_addchannel()); 39 | $application->add(new cli_cmds\bot_delchannel()); 40 | 41 | $application->add(new cli_cmds\network_add()); 42 | $application->add(new cli_cmds\network_del()); 43 | $application->add(new cli_cmds\network_list()); 44 | $application->add(new cli_cmds\network_set()); 45 | 46 | $application->add(new cli_cmds\server_add()); 47 | $application->add(new cli_cmds\server_del()); 48 | $application->add(new cli_cmds\server_set()); 49 | 50 | $application->add(new cli_cmds\showdb()); 51 | 52 | $application->add(new scripts\linktitles\cli_cmds\ignore_add()); 53 | $application->add(new scripts\linktitles\cli_cmds\ignore_list()); 54 | $application->add(new scripts\linktitles\cli_cmds\ignore_del()); 55 | $application->add(new scripts\linktitles\cli_cmds\ignore_test()); 56 | $application->add(new scripts\linktitles\cli_cmds\hostignore_add()); 57 | $application->add(new scripts\linktitles\cli_cmds\hostignore_list()); 58 | $application->add(new scripts\linktitles\cli_cmds\hostignore_del()); 59 | $application->add(new scripts\linktitles\cli_cmds\hostignore_test()); 60 | 61 | 62 | $application->run(); 63 | -------------------------------------------------------------------------------- /artbot_rest_server.php: -------------------------------------------------------------------------------- 1 | logger = new Logger('server'); 35 | $this->logger->pushHandler($logHandler); 36 | } 37 | 38 | public function initRestServer() { 39 | global $config; 40 | if(!isset($config['listen'])) { 41 | return null; 42 | } 43 | if(isset($config['listen_cert'])) { 44 | $cert = new Socket\Certificate($config['listen_cert']); 45 | $context = (new Socket\BindContext) 46 | ->withTlsContext((new Socket\ServerTlsContext)->withDefaultCertificate($cert)); 47 | } else { 48 | $context = null; 49 | } 50 | 51 | $this->server = SocketHttpServer::createForDirectAccess($this->logger); 52 | 53 | if(is_array($config['listen'])) { 54 | foreach ($config['listen'] as $address) { 55 | $this->server->expose($address, $context); 56 | } 57 | } else { 58 | $this->server->expose($config['listen'], $context); 59 | } 60 | 61 | 62 | $arrayConfig = [ 63 | 'origins' => ['*'], 64 | 'allowed_methods' => ['GET', 'POST', 'PUT'], 65 | 'max_age' => 8600, 66 | 'allowed_headers' => ['content-type'], 67 | 'exposable_headers' => ['content-type'], 68 | 'allow_credentials' => false 69 | ]; 70 | 71 | $loader = new SimpleConfigurationLoader(new ArrayConfiguration($arrayConfig)); 72 | $middleware = new CorsMiddleware($loader); 73 | 74 | $this->errorHandler = new DefaultErrorHandler(); 75 | $this->restRouter = new Router($this->server, $this->logger, $this->errorHandler); 76 | 77 | $this->stack = stackMiddleware($this->restRouter, $middleware); 78 | 79 | $this->setupRoutes(); 80 | 81 | return $this->server; 82 | } 83 | 84 | public function start() { 85 | if(isset($this->server)) 86 | $this->server->start($this->stack, $this->errorHandler); 87 | } 88 | 89 | public function stop() { 90 | if(isset($this->server)) 91 | $this->server->stop(); 92 | } 93 | 94 | public function addRoute(string $method, string $uri, RequestHandler $requestHandler) { 95 | global $config; 96 | if(!isset($config['listen'])) { 97 | return null; 98 | } 99 | $this->restRouter->addRoute($method, $uri, $requestHandler); 100 | } 101 | 102 | private function setupRoutes() { 103 | $this->restRouter->addRoute("POST", "/privmsg/{chan}", new ClosureRequestHandler( 104 | function (Request $request): Response { 105 | $notifier_keys = Yaml::parseFile(__DIR__. '/notifier_keys.yaml'); 106 | $key = $request->getHeader('key'); 107 | if (isset($notifier_keys[$key])) { 108 | echo "Request from $notifier_keys[$key] ($key)\n"; 109 | } else { 110 | echo \Irc\stripForTerminal("Blocked request for bad key $key\n"); 111 | return new Response(HttpStatus::FORBIDDEN, [ 112 | "content-type" => "text/plain; charset=utf-8" 113 | ], "Invalid key"); 114 | } 115 | $args = $request->getAttribute(Router::class); 116 | if(!isset($args['chan'])) { // todo not sure if needed 117 | return new Response(HttpStatus::BAD_REQUEST, [ 118 | "content-type" => "text/plain; charset=utf-8" 119 | ], "Must specify a chan to privmsg"); 120 | } 121 | $chan = "#{$args['chan']}"; 122 | $msg = $request->getBody()->buffer(); 123 | $msg = str_replace("\r", "\n", $msg); 124 | $msg = explode("\n", $msg); 125 | pumpToChan($chan, $msg); 126 | 127 | return new Response(HttpStatus::OK, [ 128 | "content-type" => "text/plain; charset=utf-8" 129 | ], "PRIVMSG sent\n"); 130 | })); 131 | } 132 | } -------------------------------------------------------------------------------- /artbot_scripts/artfart.php: -------------------------------------------------------------------------------- 1 | pm($args->chan, "\2artfart:\2 artfart db not found"); 18 | return; 19 | } 20 | try { 21 | $xml = simplexml_load_file($filename); 22 | if($xml === false) 23 | throw new \Exception("couldn't understand artfart db"); 24 | 25 | $fart = null; 26 | $id = $cmdArgs->getArg("id"); 27 | if($id != null) { 28 | $id = (int)(string)$id; 29 | foreach($xml->farts->fart as $f) { 30 | if((int)($f->number) == $id-1) { 31 | $fart = $f; 32 | break; 33 | } 34 | } 35 | if($fart === null) 36 | throw new \Exception("couldn't find that artfart id"); 37 | } else { 38 | //can't use array_rand on xml element 39 | $fart = $xml->farts->fart[random_int(0, count($xml->farts->fart) - 1)]; 40 | } 41 | $title = (string)$fart->full . ' - ' . (string)$fart->author; 42 | $fart = (string)$fart->content; 43 | if($cmdArgs->optEnabled('--rnb') || $cmdArgs->optEnabled('--rainbow')) 44 | $fart = \knivey\ircTools\diagRainbow($fart); 45 | $fart = explode("\n", $fart); 46 | $fart = array_map(rtrim(...), $fart); 47 | array_unshift($fart, $title); 48 | pumpToChan($args->chan, $fart); 49 | } catch (\Exception $error) { 50 | echo $error->getMessage(); 51 | $bot->pm($args->chan, "\2artfart:\2 {$error->getMessage()}"); 52 | } 53 | } -------------------------------------------------------------------------------- /artbot_scripts/bashorg.php: -------------------------------------------------------------------------------- 1 | getMessage(); 19 | $bot->pm($args->chan, "bash.org error: {$e->getMessage()}"); 20 | return; 21 | } 22 | if(empty($bashdb)) 23 | return; 24 | $id = array_rand($bashdb); 25 | $quote = $bashdb[$id]; 26 | unset($bashdb[$id]); 27 | $head = "\2Bash.org (\2$id\2):"; 28 | $quote = array_map(fn($it) => " $it", $quote); 29 | pumpToChan($args->chan, [$head, ...$quote]); 30 | } 31 | 32 | function populateBash() { 33 | global $bashdb; 34 | if(!empty($bashdb)) 35 | return; 36 | $body = async_get_contents("http://www.bash.org/?random"); 37 | $html = new HtmlDocument($body); 38 | 39 | $ids = []; 40 | foreach($html->find("p.quote") as $i) { 41 | $ids[] = $i->find("a", 0)->plaintext; 42 | } 43 | 44 | $quotes = []; 45 | foreach($html->find("p.qt") as $i) { 46 | $quote = $i->innertext; 47 | $quote = preg_split('@
@i', $quote); 48 | $quotes[] = array_filter(array_map(function ($quote) { 49 | $quote = htmlspecialchars_decode($quote, ENT_QUOTES | ENT_HTML5); 50 | return trim(str_replace(["\r", "\n"], '', $quote)); 51 | }, $quote)); 52 | } 53 | if(count($ids) != count($quotes)) 54 | throw new \Exception("Weird data trying to extract quotes."); 55 | $bashdb = array_combine($ids, $quotes); 56 | if (count($bashdb) == 0) 57 | throw new \Exception("Couldn't extract any quotes from site."); 58 | } -------------------------------------------------------------------------------- /artbot_scripts/help.php: -------------------------------------------------------------------------------- 1 | optEnabled("--priv")) { 20 | if (!$router->cmdExists($cmdArgs['command'])) { 21 | $bot->msg($args->chan, "no command by that name"); 22 | return; 23 | } 24 | $help = (string)$router->cmds[$cmdArgs['command']]; 25 | } else { 26 | if (!$router->cmdExistsPriv($cmdArgs['command'])) { 27 | $bot->msg($args->chan, "no command by that name"); 28 | return; 29 | } 30 | $help = (string)$router->privCmds[$cmdArgs['command']]; 31 | } 32 | showHelp($args->chan, $bot, $help); 33 | return; 34 | } 35 | $first = 0; 36 | if($cmdArgs->optEnabled("--priv")) 37 | $cmds = $router->privCmds; 38 | else 39 | $cmds = $router->cmds; 40 | $out = ""; 41 | foreach ($cmds as $cmd) { 42 | if($first++) 43 | $out .= "---\n"; 44 | $out .= (string)$cmd . "\n"; 45 | } 46 | showHelp($args->chan, $bot, $out); 47 | } 48 | 49 | function showHelp(string $chan, $bot, string $lines) { 50 | if(function_exists('pumpToChan')) { 51 | pumpToChan($chan, explode("\n", $lines)); 52 | return; 53 | } 54 | if(strlen($lines) > 1000) { 55 | try { 56 | $connectContext = (new \Amp\Socket\ConnectContext) 57 | ->withConnectTimeout(15); 58 | $sock = \Amp\Socket\connect("tcp://termbin.com:9999", $connectContext); 59 | $sock->write($lines); 60 | $url = \Amp\ByteStream\buffer($sock); 61 | $sock->end(); 62 | $bot->msg($chan, "help: $url"); 63 | } catch (Exception $e) { 64 | $bot->msg($chan, "trouble uploading help :("); 65 | } 66 | return; 67 | } 68 | foreach(explode("\n", $lines) as $line) { 69 | $bot->msg($chan, $line); 70 | } 71 | } -------------------------------------------------------------------------------- /artconfig.example.yaml: -------------------------------------------------------------------------------- 1 | name: van 2 | trigger: "@" 3 | server: localhost 4 | port: 6697 5 | ssl: true 6 | throttle: false 7 | bindIp: 0 8 | channels: ['#knivey','#h4x'] 9 | artdir: "/home/knivey/ircart/" 10 | pumplag: 25 11 | #a2m: ~/a2m/a2m 12 | #p2u: ~/p2u/p2u 13 | 14 | # can override default if sharing with other bots 15 | #quotedb: "/home/knivey/quote.db" 16 | 17 | # used for recording art with http post 18 | listen: "0.0.0.0:1212" 19 | # I put the above listen behind a nginx reverse proxy so that it can easily have ssl and a nice url 20 | # this is used for the bot to show a url 21 | rest_url: "https://arts.h4x.life/pn" 22 | #listen_cert: "cert.pem" 23 | 24 | #used for --link option so people may downlaod art file 25 | link_url: "https://art.h4x.life/" 26 | 27 | # for the @url and @img commands when used with an image (does p2u) 28 | url_default_width: 80 29 | 30 | # For the trash command to delete art 31 | trustedNetwork: false 32 | trashDir: "/home/knivey/trashed" 33 | 34 | #artfiles played back from these dirs will be word wrapped instead of the default (cut off) 35 | wordwrap_dirs: 36 | - "h4x/aibird" -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 'pdo_pgsql', 36 | // 'user' => 'lolbot', 37 | // 'dbname' => 'lolbot', 38 | // 'password' => 'lolpass', 39 | // 'host' => 'localhost', 40 | // 'port' => 5432, 41 | // 'charset' => 'utf-8' 42 | //), $ORMconfig); 43 | 44 | 45 | /* 46 | $conn = DriverManager::getConnection([ 47 | 'driver' => 'pdo_sqlite', 48 | 'path' => __DIR__ . '/db.sqlite', 49 | ], $ORMconfig); 50 | */ 51 | 52 | if($conn->getDatabasePlatform()::class == \Doctrine\DBAL\Platforms\SqlitePlatform::class) 53 | $conn->executeStatement("PRAGMA foreign_keys=ON"); 54 | /** 55 | * @psalm-suppress InvalidGlobal 56 | */ 57 | global $entityManager; 58 | $entityManager = new EntityManager($conn, $ORMconfig); 59 | 60 | 61 | $migrationConfig = new YamlFile('migrations.yml'); 62 | $dependencyFactory = DependencyFactory::fromEntityManager($migrationConfig, new ExistingEntityManager($entityManager)); 63 | 64 | function dieIfPendingMigration(): void 65 | { 66 | global $dependencyFactory; 67 | $availMigrations = $dependencyFactory->getMigrationStatusCalculator()->getNewMigrations(); 68 | if ($availMigrations->count() != 0) { 69 | die("EXITING: You have pending migrations to execute\n Use the vendor/bin/doctrine-migrations tool first\n"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cli-config.php: -------------------------------------------------------------------------------- 1 | addOption('network', "N", InputOption::VALUE_REQUIRED, 'Network ID') 25 | ->addArgument("name", InputArgument::REQUIRED) 26 | ; 27 | } 28 | 29 | public function execute(InputInterface $input, OutputInterface $output): int { 30 | global $entityManager; 31 | if($input->getOption("network") === null) { 32 | throw new \InvalidArgumentException("Must specify a network"); 33 | } 34 | 35 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 36 | if($network === null) { 37 | throw new \InvalidArgumentException("Couldn't find that network ID"); 38 | } 39 | 40 | $bot = new Bot(); 41 | $bot->name = $input->getArgument("name"); 42 | $bot->network = $network; 43 | $entityManager->persist($bot); 44 | $entityManager->flush(); 45 | 46 | showdb::showdb(); 47 | 48 | return Command::SUCCESS; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cli_cmds/bot_addchannel.php: -------------------------------------------------------------------------------- 1 | addArgument("bot", InputArgument::REQUIRED, "ID of the bot"); 24 | $this->addArgument("channel", InputArgument::REQUIRED); 25 | //$this->addOption("key", "k", InputOption::VALUE_REQUIRED); 26 | } 27 | 28 | protected function execute(InputInterface $input, OutputInterface $output): int { 29 | global $entityManager; 30 | $bot = $entityManager->getRepository(Bot::class)->find($input->getArgument("bot")); 31 | if(!$bot) { 32 | throw new \InvalidArgumentException("Bot ID not found"); 33 | } 34 | 35 | $channel = new Channel(); 36 | $channel->name = $input->getArgument("channel"); 37 | $bot->addChannel($channel); 38 | 39 | $entityManager->persist($channel); 40 | $entityManager->flush(); 41 | 42 | showdb::showdb(); 43 | 44 | return Command::SUCCESS; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cli_cmds/bot_del.php: -------------------------------------------------------------------------------- 1 | addArgument("bot", InputArgument::REQUIRED, "ID of the bot to delete"); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | global $entityManager; 26 | $repo = $entityManager->getRepository(Bot::class); 27 | $bot = $repo->find($input->getArgument('bot')); 28 | if($bot == null) { 29 | throw new \InvalidArgumentException("Couldn't find a bot with that ID"); 30 | } 31 | 32 | $entityManager->remove($bot); 33 | $entityManager->flush(); 34 | 35 | showdb::showdb(); 36 | 37 | return Command::SUCCESS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli_cmds/bot_delchannel.php: -------------------------------------------------------------------------------- 1 | addArgument("channel", InputArgument::REQUIRED, "ID of the channel"); 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output): int { 27 | global $entityManager; 28 | $channel = $entityManager->getRepository(Channel::class)->find($input->getArgument("channel")); 29 | if(!$channel) { 30 | throw new \InvalidArgumentException("Channel ID not found"); 31 | } 32 | 33 | $entityManager->remove($channel); 34 | $entityManager->flush(); 35 | 36 | showdb::showdb(); 37 | 38 | return Command::SUCCESS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cli_cmds/bot_list.php: -------------------------------------------------------------------------------- 1 | getRepository(Bot::class); 23 | $bots = $repo->findAll(); 24 | foreach ($bots as $bot) { 25 | $output->writeln($bot); 26 | } 27 | return Command::SUCCESS; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cli_cmds/bot_set.php: -------------------------------------------------------------------------------- 1 | addArgument("bot", InputArgument::REQUIRED, "Bot ID"); 31 | $this->addArgument("setting", InputArgument::OPTIONAL, "setting name"); 32 | $this->addArgument("value", InputArgument::OPTIONAL, "New value"); 33 | } 34 | 35 | //probably could make a generic base command class for settings 36 | 37 | protected function execute(InputInterface $input, OutputInterface $output): int { 38 | global $entityManager; 39 | $bot = $entityManager->getRepository(Bot::class)->find($input->getArgument("bot")); 40 | if(!$bot) { 41 | throw new \InvalidArgumentException("Server by that ID not found"); 42 | } 43 | 44 | if($input->getArgument("setting") === null) { 45 | $this->showsets($input, $output, $bot); 46 | return Command::SUCCESS; 47 | } 48 | 49 | if(!in_array($input->getArgument("setting"), $this->settings)) { 50 | throw new \InvalidArgumentException("No setting by that name"); 51 | } 52 | 53 | if($input->getArgument("value") === null) { 54 | $this->showsets($input, $output, $bot); 55 | return Command::SUCCESS; 56 | } 57 | 58 | $bot->{$input->getArgument("setting")} = $input->getArgument("value"); 59 | 60 | 61 | $entityManager->persist($bot); 62 | $entityManager->flush(); 63 | 64 | $this->showsets($input, $output, $bot); 65 | 66 | return Command::SUCCESS; 67 | } 68 | 69 | function showsets(InputInterface $input, OutputInterface $output, $bot) { 70 | $io = new SymfonyStyle($input, $output); 71 | $rows = []; 72 | foreach ($this->settings as $setting) { 73 | $rows[] = [$setting, $bot->$setting]; 74 | } 75 | $io->table( 76 | ["Setting", "Value"], 77 | $rows 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cli_cmds/ignore_add.php: -------------------------------------------------------------------------------- 1 | addOption('network', "N", InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Network ID') 26 | ->addArgument("hostmask", InputArgument::REQUIRED) 27 | ->addArgument("reason", InputArgument::OPTIONAL) 28 | ; 29 | } 30 | 31 | protected function execute(InputInterface $input, OutputInterface $output): int { 32 | global $entityManager; 33 | 34 | if(count($input->getOption("network")) == 0) { 35 | throw new \InvalidArgumentException("Must specify a network"); 36 | } 37 | 38 | $ignore = new Ignore(); 39 | $ignore->hostmask = $input->getArgument('hostmask'); 40 | if($input->getArgument('reason') !== null) 41 | $ignore->reason = $input->getArgument('reason'); 42 | 43 | /** @var EntityRepository $repo */ 44 | $repo = $entityManager->getRepository(Network::class); 45 | foreach($input->getOption("network") as $net) { 46 | $network = $repo->find($net); 47 | if ($network === null) { 48 | throw new \InvalidArgumentException("Couldn't find that network ID ($net)"); 49 | } 50 | $ignore->addToNetwork($network); 51 | } 52 | 53 | $entityManager->persist($ignore); 54 | $entityManager->flush(); 55 | 56 | showdb::showdb(); 57 | 58 | return Command::SUCCESS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cli_cmds/ignore_addnetwork.php: -------------------------------------------------------------------------------- 1 | addOption('network', "N", InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Network ID') 25 | ->addArgument("ignore", InputArgument::REQUIRED, "ID of the ignore") 26 | ; 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output): int { 30 | global $entityManager; 31 | if(count($input->getOption("network")) == 0) { 32 | throw new \InvalidArgumentException("Must specify a network"); 33 | } 34 | 35 | $ignore = $entityManager->getRepository(Ignore::class)->find($input->getArgument("ignore")); 36 | if($ignore === null) { 37 | throw new \InvalidArgumentException("Couldn't find that ignore ID"); 38 | } 39 | 40 | /** @var EntityRepository $repo */ 41 | $repo = $entityManager->getRepository(Network::class); 42 | foreach($input->getOption("network") as $net) { 43 | $network = $repo->find($net); 44 | if ($network === null) { 45 | throw new \InvalidArgumentException("Couldn't find that network ID ($net)"); 46 | } 47 | $ignore->addToNetwork($network); 48 | } 49 | 50 | $entityManager->persist($ignore); 51 | $entityManager->flush(); 52 | 53 | showdb::showdb(); 54 | 55 | return Command::SUCCESS; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cli_cmds/ignore_del.php: -------------------------------------------------------------------------------- 1 | addArgument("ignore", InputArgument::REQUIRED, "ID of the ignore"); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | global $entityManager; 26 | $repo = $entityManager->getRepository(Ignore::class); 27 | $ignore = $repo->find($input->getArgument('ignore')); 28 | if($ignore == null) { 29 | throw new \InvalidArgumentException("Couldn't find and ignore by that ID"); 30 | } 31 | $entityManager->remove($ignore); 32 | $entityManager->flush(); 33 | 34 | showdb::showdb(); 35 | 36 | return Command::SUCCESS; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli_cmds/ignore_list.php: -------------------------------------------------------------------------------- 1 | addOption('network', "N", InputOption::VALUE_REQUIRED, 'Network ID') 24 | ->addOption('orphaned', "o", InputOption::VALUE_NONE, 'Only display orphans') 25 | ; 26 | } 27 | 28 | protected function execute(InputInterface $input, OutputInterface $output): int { 29 | global $entityManager; 30 | if(null !== $id = $input->getOption("network")) { 31 | if (null === $network = $entityManager->getRepository(Network::class)->find($id)) { 32 | throw new \InvalidArgumentException("Network by that ID not found"); 33 | } 34 | $this->print_ignores($network->getIgnores(), $output); 35 | return Command::SUCCESS; 36 | } 37 | $repo = $entityManager->getRepository(Ignore::class); 38 | $ignores = $repo->findAll(); 39 | if($input->getOption("orphaned") !== null){ 40 | $ignores = array_filter($ignores, fn ($i) => count($i->getNetworks()) == 0); 41 | } 42 | $this->print_ignores($ignores, $output); 43 | return Command::SUCCESS; 44 | } 45 | 46 | function print_ignores($ignores, OutputInterface $output) { 47 | foreach ($ignores as $ignore) { 48 | $output->writeln($ignore); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cli_cmds/ignore_test.php: -------------------------------------------------------------------------------- 1 | addOption('network', "N", InputOption::VALUE_REQUIRED, 'Network ID') 23 | ->addArgument("host", InputArgument::REQUIRED, "Hostname to test if any ignores match") 24 | ; 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output): int { 28 | global $entityManager; 29 | /** 30 | * @var \lolbot\entities\IgnoreRepository $ignoreRepository 31 | */ 32 | $ignoreRepository = $entityManager->getRepository(Ignore::class); 33 | 34 | if($input->getOption("network") !== null) { 35 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 36 | if($network === null) { 37 | throw new \InvalidArgumentException("Couldn't find that network ID"); 38 | } 39 | $ignores = $ignoreRepository->findMatching($input->getArgument('host'), $network); 40 | } else { 41 | $ignores = $ignoreRepository->findMatching($input->getArgument('host')); 42 | } 43 | foreach ($ignores as $ignore) { 44 | $output->writeln($ignore); 45 | } 46 | return Command::SUCCESS; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cli_cmds/network_add.php: -------------------------------------------------------------------------------- 1 | addArgument("name", InputArgument::REQUIRED, "Name for the network"); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | global $entityManager; 26 | $network = $entityManager->getRepository(Network::class)->findOneBy(["name" => $input->getArgument("name")]); 27 | if($network !== null) { 28 | throw new \InvalidArgumentException("Network already exists with that name"); 29 | } 30 | 31 | $network = new Network(); 32 | $network->name = $input->getArgument("name"); 33 | $entityManager->persist($network); 34 | $entityManager->flush(); 35 | 36 | showdb::showdb(); 37 | 38 | return Command::SUCCESS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cli_cmds/network_del.php: -------------------------------------------------------------------------------- 1 | addArgument("network", InputArgument::REQUIRED, "ID of the network"); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | global $entityManager; 26 | $repo = $entityManager->getRepository(Network::class); 27 | $network = $repo->find($input->getArgument('network')); 28 | if($network == null) { 29 | throw new \InvalidArgumentException("Couldn't find that network ID"); 30 | } 31 | 32 | $entityManager->remove($network); 33 | $entityManager->flush(); 34 | 35 | showdb::showdb(); 36 | 37 | return Command::SUCCESS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli_cmds/network_list.php: -------------------------------------------------------------------------------- 1 | getRepository(Network::class); 23 | $networks = $repo->findAll(); 24 | foreach ($networks as $network) { 25 | $output->writeln($network); 26 | } 27 | return Command::SUCCESS; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cli_cmds/network_set.php: -------------------------------------------------------------------------------- 1 | addArgument("network", InputArgument::REQUIRED, "Network ID"); 25 | $this->addArgument("setting", InputArgument::OPTIONAL, "setting name"); 26 | $this->addArgument("value", InputArgument::OPTIONAL, "New value"); 27 | } 28 | 29 | //probably could make a generic base command class for settings 30 | 31 | protected function execute(InputInterface $input, OutputInterface $output): int { 32 | global $entityManager; 33 | $network = $entityManager->getRepository(Network::class)->find($input->getArgument("network")); 34 | if(!$network) { 35 | throw new \InvalidArgumentException("Network by that ID not found"); 36 | } 37 | 38 | if($input->getArgument("setting") === null) { 39 | $output->writeln($network); 40 | } 41 | 42 | if(!in_array($input->getArgument("setting"), $this->settings)) { 43 | throw new \InvalidArgumentException("No network setting by that name"); 44 | } 45 | 46 | if($input->getArgument("value") === null) { 47 | $output->writeln($network); 48 | } 49 | 50 | $network->{$input->getArgument("setting")} = $input->getArgument("value"); 51 | 52 | 53 | $entityManager->persist($network); 54 | $entityManager->flush(); 55 | 56 | showdb::showdb(); 57 | 58 | return Command::SUCCESS; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cli_cmds/server_add.php: -------------------------------------------------------------------------------- 1 | addArgument("network", InputArgument::REQUIRED, "ID of the network"); 23 | $this->addArgument("address", InputArgument::REQUIRED, "Address of the server, excluding port (Ex: irc.gamesurge.net)"); 24 | $this->addOption("port", "p", InputOption::VALUE_REQUIRED); 25 | $this->addOption("ssl", "s", InputOption::VALUE_NONE); 26 | $this->addOption("no-throttle", "", InputOption::VALUE_NONE, "Should the messages to the server not be rate limited"); 27 | $this->addOption("password", "", InputOption::VALUE_REQUIRED); 28 | } 29 | 30 | protected function execute(InputInterface $input, OutputInterface $output): int { 31 | global $entityManager; 32 | $network = $entityManager->getRepository(Network::class)->find($input->getArgument("network")); 33 | if(!$network) { 34 | throw new \InvalidArgumentException("Network ID not found"); 35 | } 36 | 37 | $server = new Server(); 38 | $server->address = $input->getArgument("address"); 39 | $server->setNetwork($network); 40 | 41 | if($port = $input->getOption("port")) { 42 | if((int)$port <= 0 || $port > 65536) { 43 | throw new \InvalidArgumentException("Invalid port"); 44 | } 45 | $server->port = (int)$port; 46 | } 47 | 48 | if($input->getOption("ssl")) { 49 | $server->ssl = true; 50 | } 51 | 52 | if($input->getOption("no-throttle")) { 53 | $server->throttle = false; 54 | } 55 | 56 | if($input->getOption("password")) { 57 | $server->password = $input->getOption("password"); 58 | } 59 | 60 | $entityManager->persist($server); 61 | $entityManager->flush(); 62 | 63 | showdb::showdb(); 64 | 65 | return Command::SUCCESS; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cli_cmds/server_del.php: -------------------------------------------------------------------------------- 1 | addArgument("server", InputArgument::REQUIRED, "ID of the server to delete"); 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output): int { 25 | global $entityManager; 26 | $repo = $entityManager->getRepository(Server::class); 27 | $server = $repo->find($input->getArgument('server')); 28 | if($server == null) { 29 | throw new \InvalidArgumentException("Couldn't find a server with that ID"); 30 | } 31 | 32 | $entityManager->remove($server); 33 | $entityManager->flush(); 34 | 35 | showdb::showdb(); 36 | 37 | return Command::SUCCESS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli_cmds/server_set.php: -------------------------------------------------------------------------------- 1 | addArgument("server", InputArgument::REQUIRED, "Server ID"); 29 | $this->addArgument("setting", InputArgument::OPTIONAL, "setting name"); 30 | $this->addArgument("value", InputArgument::OPTIONAL, "New value"); 31 | } 32 | 33 | //probably could make a generic base command class for settings 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int { 36 | global $entityManager; 37 | $server = $entityManager->getRepository(Server::class)->find($input->getArgument("server")); 38 | if(!$server) { 39 | throw new \InvalidArgumentException("Server by that ID not found"); 40 | } 41 | 42 | if($input->getArgument("setting") === null) { 43 | $output->writeln($server); 44 | return Command::SUCCESS; 45 | } 46 | 47 | if(!in_array($input->getArgument("setting"), $this->settings)) { 48 | throw new \InvalidArgumentException("No setting by that name"); 49 | } 50 | 51 | if($input->getArgument("value") === null) { 52 | $output->writeln($server); 53 | return Command::SUCCESS; 54 | } 55 | 56 | $server->{$input->getArgument("setting")} = $input->getArgument("value"); 57 | 58 | 59 | $entityManager->persist($server); 60 | $entityManager->flush(); 61 | 62 | showdb::showdb(); 63 | 64 | return Command::SUCCESS; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cli_cmds/showdb.php: -------------------------------------------------------------------------------- 1 | getRepository(Network::class)->findAll(); 31 | foreach ($networks as $network) { 32 | echo " " . $network . "\n"; 33 | echo " bots:\n"; 34 | foreach ($network->getBots() as $bot) { 35 | echo " " . $bot . "\n"; 36 | } 37 | echo " ignores:\n"; 38 | foreach ($network->getIgnores() as $ignore) { 39 | echo " " . $ignore . "\n"; 40 | } 41 | echo " servers:\n"; 42 | foreach ($network->getServers() as $server) { 43 | echo " " . $server . "\n"; 44 | } 45 | } 46 | 47 | echo "\nbots:\n"; 48 | $bots = $entityManager->getRepository(Bot::class)->findAll(); 49 | foreach ($bots as $bot) { 50 | echo " " . $bot . "\n"; 51 | echo " network: " . $bot->network->name . "\n"; 52 | echo " channels: " . implode(", ", $bot->getChannels()->toArray()) . "\n"; 53 | } 54 | 55 | echo "\nignores:\n"; 56 | $ignores = $entityManager->getRepository(Ignore::class)->findAll(); 57 | foreach ($ignores as $ignore) { 58 | echo " " . $ignore . "\n"; 59 | echo " networks: "; 60 | foreach ($ignore->getNetworks() as $network) 61 | echo $network->id . ", "; 62 | echo "\n"; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=8.1", 4 | "ext-imagick": "*", 5 | "ext-json": "*", 6 | "ext-mbstring": "*", 7 | "ext-pcntl": "*", 8 | "ext-pdo": "*", 9 | "ext-pdo_sqlite": "*", 10 | "ext-simplexml": "*", 11 | "amphp/amp": "^3", 12 | "amphp/file": "^3.1", 13 | "amphp/http-client": "^5", 14 | "amphp/http-client-cookies": "^2", 15 | "amphp/http-server": "^3", 16 | "amphp/http-server-router": "^2", 17 | "amphp/log": "^2", 18 | "amphp/socket": "^2", 19 | "crell/tukio": "^1.3", 20 | "cspray/labrador-http-cors": "^1", 21 | "decidedly/decidedly-markov": "^1.1", 22 | "doctrine/migrations": "^3.7", 23 | "doctrine/orm": "^2.17", 24 | "gabordemooij/redbean": "^5.7", 25 | "itwmw/color-difference": "^1.2", 26 | "json-mapper/json-mapper": "^2.22", 27 | "khill/php-duration": "~1.0", 28 | "knivey/cmdr": "^4.0", 29 | "knivey/irctools": "dev-master", 30 | "knivey/reddit": "^1.0", 31 | "knivey/tools": "dev-master", 32 | "league/uri": "^7", 33 | "monolog/monolog": "^2.3", 34 | "nesbot/carbon": "^2.51", 35 | "simplehtmldom/simplehtmldom": "dev-master", 36 | "symfony/cache": "^6.0", 37 | "symfony/console": "^6.4", 38 | "symfony/finder": "^7.1", 39 | "symfony/string": "^7.0", 40 | "symfony/yaml": "^5.0" 41 | }, 42 | "name": "knivey/lolbot", 43 | "description": "IRC bot made for fun for friends", 44 | "authors": [ 45 | { 46 | "name": "knivey", 47 | "email": "knivey@botops.net" 48 | } 49 | ], 50 | "autoload": { 51 | "psr-4": { 52 | "scripts\\": "scripts", 53 | "Irc\\": "library/Irc", 54 | "draw\\": "library/draw", 55 | "lolbot\\entities\\": "entities", 56 | "lolbot\\cli_cmds\\": "cli_cmds" 57 | }, 58 | "files": [ 59 | "library/Irc/Consts.php" 60 | ] 61 | }, 62 | "require-dev": { 63 | "ramsey/composer-repl": "^1.5", 64 | "phpstan/phpstan": "^2.0", 65 | "friendsofphp/php-cs-fixer": "^3.64" 66 | }, 67 | "config": { 68 | "allow-plugins": { 69 | "ramsey/composer-repl": true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | 2 | #per bot config items 3 | bots: 4 | 2: #bot id from database 5 | # Should the bot fetch urls and display the title of the page 6 | linktitles: true 7 | # Uncomment to start the notification listen server 8 | # Notifications can be sent to the bot using one of the keys in scripts/notifier/notifier_keys.yaml 9 | # echo "test" | curl -H "key: example" -X POST --data-binary @- http://127.0.0.1:1337/privmsg/channel 10 | #listen: [ 11 | # "0.0.0.0:1337", 12 | # "[::]:1337" 13 | #] 14 | # Set to true to enable codesand (configure server in scripts/codesand/config.yaml) 15 | codesand: false 16 | codesand_maxlines: 30 17 | #pump_host 18 | #pump_key 19 | url_log_chan: "#urls" 20 | #youtube_pump_host: 21 | #youtube_pump_key: 22 | # If the bot should use p2u to send a thumbnail to channel 23 | #youtube_thumb: true 24 | #youtube_thumbwidth: 60 25 | 26 | 27 | #configure the database (follows doctrine config) 28 | database: 29 | driver: "pdo_pgsql" 30 | user: lolbot 31 | dbname: lolbot 32 | #database: 33 | # driver: "pdo_sqlite" 34 | # path: "db.sqlite" 35 | 36 | # key for wolfram alpha 37 | # https://products.wolframalpha.com/api/ 38 | #waKey: 'key' 39 | 40 | 41 | # key for iex 42 | # https://www.iexcloud.io/pricing/ (Under the two plans there's a just getting start 50,000 credits free plan) 43 | #iexKey: 'key' 44 | 45 | 46 | # key for bing and its options 47 | # https://azure.microsoft.com/en-us/pricing/details/cognitive-services/search-api/ 48 | #bingKey: 'key' 49 | #bingEP: "https://api.cognitive.microsoft.com/bing/v7.0/" 50 | #bingLang: "en-US" 51 | 52 | 53 | # openweather key 54 | # https://openweathermap.org/price 55 | #openweatherKey: "key" 56 | # Weather uses bing maps to make the location search into lat,long for the lookups 57 | #bingMapsKey: "key" 58 | 59 | 60 | # Twitter API (for link info) 61 | #twitter_bearer: "key" 62 | 63 | 64 | # Youtube api key 65 | # https://support.google.com/googleapi/answer/6158862?hl=en 66 | #gkey: "key" 67 | 68 | 69 | # If enabled are set then you need to have yt-dlp 70 | youtube_download_shorts: false 71 | # the bot will download youtube shorts less than 1 minute in length and save to a folder 72 | # the url is used to make a direct link for display in channel 73 | #youtube_host_shorts: "/home/knivey/files/" 74 | #youtube_host_shorts_url: "https://files.h4x.life/" 75 | #If shorts are enabled and the above options not set the bot will try to upload to filehole service 76 | 77 | # Location of p2u binary 78 | # https://git.trollforge.org/p2u/ 79 | # rehosted at https://github.com/knivey/p2u 80 | #p2u: "~/p2u/p2u" 81 | 82 | 83 | # Last.fm API key 84 | # https://www.last.fm/api/account/create 85 | #lastfm: key 86 | 87 | 88 | # namecheap API user/key 89 | #namecheap_key: key 90 | #namecheap_user: user 91 | 92 | 93 | # username:password on a github account, not strictly required but will greatly increase api limits 94 | # Can just register a throwaway github for this 95 | #github_auth: "" 96 | 97 | -------------------------------------------------------------------------------- /doctrine-cli.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 49 | */ 50 | #[ORM\OneToMany(targetEntity: Channel::class, mappedBy: "bot")] 51 | protected Collection $channels; 52 | 53 | public function __construct() 54 | { 55 | $this->created = new \DateTimeImmutable(); 56 | $this->channels = new ArrayCollection(); 57 | } 58 | 59 | public function addChannel(Channel $channel) { 60 | $channel->bot = $this; 61 | $this->channels[] = $channel; 62 | } 63 | 64 | public function getChannels() { 65 | return $this->channels; 66 | } 67 | 68 | public function __toString(): string 69 | { 70 | return "id: $this->id name: $this->name created: ".$this->created->format('r'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /entities/Channel.php: -------------------------------------------------------------------------------- 1 | id}:{$this->name}"; 26 | } 27 | } -------------------------------------------------------------------------------- /entities/Ignore.php: -------------------------------------------------------------------------------- 1 | hostmask) . 'i', $nickHost); 38 | } 39 | 40 | public function assignedToNetwork(Network $network): bool { 41 | return $this->networks->contains($network); 42 | } 43 | 44 | public function addToNetwork(Network $network) { 45 | $network->addIgnore($this); 46 | $this->networks[] = $network; 47 | } 48 | 49 | public function getNetworks() { 50 | return $this->networks; 51 | } 52 | 53 | public function __construct() 54 | { 55 | $this->created = new \DateTimeImmutable(); 56 | $this->networks = new ArrayCollection(); 57 | } 58 | 59 | public function __toString(): string 60 | { 61 | return "id: $this->id hostmask: $this->hostmask reason: $this->reason created: ".$this->created->format('r'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /entities/IgnoreRepository.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class IgnoreRepository extends EntityRepository 10 | { 11 | /** 12 | * @param string $host 13 | * @param Network|null $network 14 | * @return array 15 | */ 16 | public function findMatching(string $host, Network|null $network = null): array { 17 | $ignores = $this->findAll(); 18 | if($network !== null) { 19 | $ignores = array_filter($ignores, function (Ignore $i) use ($network, $host) { 20 | return ($i->assignedToNetwork($network) && $i->matches($host)); 21 | }); 22 | } 23 | return array_filter($ignores, function (Ignore $i) use ($host) { 24 | return $i->matches($host); 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /entities/Network.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | #[ORM\OneToMany(targetEntity: Bot::class, mappedBy: "network")] 29 | protected Collection $bots; 30 | 31 | /** 32 | * @var Collection 33 | */ 34 | #[ORM\ManyToMany(targetEntity: Ignore::class, mappedBy: 'networks')] 35 | #[ORM\JoinTable(name: "Ignore_Network")] 36 | protected Collection $ignores; 37 | 38 | /** 39 | * @var Collection 40 | */ 41 | #[ORM\OneToMany(targetEntity: Server::class, mappedBy: "network")] 42 | protected Collection $servers; 43 | 44 | public function __construct() 45 | { 46 | $this->created = new \DateTimeImmutable(); 47 | $this->bots = new ArrayCollection(); 48 | $this->ignores = new ArrayCollection(); 49 | $this->servers = new ArrayCollection(); 50 | } 51 | 52 | public function addServer(Server $server) { 53 | $this->servers[] = $server; 54 | } 55 | 56 | public function getServers() { 57 | return $this->servers; 58 | } 59 | 60 | private $serverIdx = 0; 61 | public function selectServer(): ?Server { 62 | if(count($this->servers) == 0) 63 | return null; 64 | 65 | $server = $this->servers[$this->serverIdx]; 66 | $this->serverIdx++; 67 | if(!isset($this->servers[$this->serverIdx])) 68 | $this->serverIdx = 0; 69 | return $server; 70 | } 71 | 72 | public function addBot(Bot $bot) { 73 | $this->bots[] = $bot; 74 | } 75 | 76 | public function getBots() { 77 | return $this->bots; 78 | } 79 | 80 | public function getIgnores() { 81 | return $this->ignores; 82 | } 83 | 84 | public function addIgnore(Ignore $ignore) { 85 | $this->ignores[] = $ignore; 86 | } 87 | 88 | public function __toString():string { 89 | return "id: {$this->id} name: {$this->name} created: ".$this->created->format('r'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /entities/Server.php: -------------------------------------------------------------------------------- 1 | network = $network; 38 | $network->addServer($this); 39 | } 40 | 41 | public function __toString(): string 42 | { 43 | $password = "Password: " . (($this->password!==null) ? $this->password : ""); 44 | $ssl = "SSL: " . ($this->ssl ? "true" : "false"); 45 | $throttle = "Throttle: " . ($this->throttle ? "true" : "false"); 46 | return "ID: {$this->id} {$this->address}:{$this->port} $password $ssl $throttle"; 47 | } 48 | } -------------------------------------------------------------------------------- /import_old_db/aliases.php: -------------------------------------------------------------------------------- 1 | \n"); 18 | } 19 | 20 | $network = $entityManager->getRepository(Network::class)->find($network_id); 21 | if(!$network) 22 | die("Couldn't find that network_id\n"); 23 | 24 | R::addDatabase("aliases", "sqlite:{$dbfile}"); 25 | R::selectDatabase("aliases"); 26 | $aliases = R::findAll("alias"); 27 | foreach($aliases as $a) { 28 | $alias = new alias(); 29 | $alias->name = $a->name; 30 | $alias->nameLowered = (new U($a->name))->lower(); 31 | $alias->value = $a->value; 32 | $alias->chan = $a->chan; 33 | $alias->chanLowered = (new U($a->chan))->lower(); 34 | $alias->fullhost = $a->fullhost; 35 | $alias->act = $a->act; 36 | $alias->cmd = $a->cmd; 37 | $alias->network = $network; 38 | $entityManager->persist($alias); 39 | } 40 | 41 | $entityManager->flush(); 42 | 43 | echo "Aliases imported\n"; -------------------------------------------------------------------------------- /import_old_db/reminders.php: -------------------------------------------------------------------------------- 1 | \n"); 19 | } 20 | 21 | $network = $entityManager->getRepository(Network::class)->find($network_id); 22 | if(!$network) 23 | die("Couldn't find that network_id\n"); 24 | 25 | R::addDatabase("reminders", "sqlite:{$dbfile}"); 26 | R::selectDatabase("reminders"); 27 | $reminders = R::findAll("reminder"); 28 | foreach($reminders as $r) { 29 | $newReminder = new reminder(); 30 | $newReminder->nick = $r->nick; 31 | $newReminder->chan = $r->chan; 32 | $newReminder->at = $r->at; 33 | $newReminder->sent = $r->sent; 34 | $newReminder->msg = $r->msg; 35 | $newReminder->network = $network; 36 | $entityManager->persist($newReminder); 37 | } 38 | 39 | $entityManager->flush(); 40 | 41 | echo "Reminders imported\n"; -------------------------------------------------------------------------------- /import_old_db/seen.php: -------------------------------------------------------------------------------- 1 | \n"); 18 | } 19 | 20 | $network = $entityManager->getRepository(Network::class)->find($network_id); 21 | if(!$network) 22 | die("Couldn't find that network_id\n"); 23 | 24 | R::addDatabase("seen", "sqlite:{$dbfile}"); 25 | R::selectDatabase("seen"); 26 | $ss = R::findAll("seen"); 27 | $c = 0; 28 | foreach($ss as $s) { 29 | $seen = new \scripts\seen\entities\seen(); 30 | $seen->nick = u($s->orig_nick)->lower(); 31 | $seen->orig_nick = $s->orig_nick; 32 | $seen->chan = $s->chan; 33 | $seen->text = $s->text; 34 | $seen->action = $s->action; 35 | $seen->time = new \DateTime($s->time); 36 | $seen->network = $network; 37 | $entityManager->persist($seen); 38 | $c++; 39 | } 40 | 41 | $entityManager->flush(); 42 | 43 | echo "$c Seens imported\n"; -------------------------------------------------------------------------------- /import_old_db/tells.php: -------------------------------------------------------------------------------- 1 | \n"); 19 | } 20 | 21 | $network = $entityManager->getRepository(Network::class)->find($network_id); 22 | if(!$network) 23 | die("Couldn't find that network_id\n"); 24 | 25 | /* 26 | * $tell->date = R::isoDateTime(); 27 | $tell->from = $from; 28 | $tell->msg = $msg; 29 | $tell->to = strtolower($nick); 30 | $tell->sent = 0; 31 | $tell->network = $network; 32 | $tell->chan = $chan; 33 | $tell->to_net = $toNet; null means global 34 | */ 35 | 36 | R::addDatabase("tells", "sqlite:{$dbfile}"); 37 | R::selectDatabase("tells"); 38 | $tells = R::findAll("msg", "`sent` = 0"); 39 | $otherNets = []; 40 | $cnt = 0; 41 | foreach($tells as $t) { 42 | if(strtolower($t->network) != strtolower($fromNet)) { 43 | $otherNets[strtolower($t->network)] = $t->network; 44 | continue; 45 | } 46 | $tell = new tell(); 47 | $tell->created = new \DateTime($t->date); 48 | $tell->sender = $t->from; 49 | $tell->msg = $t->msg; 50 | $tell->target = u(strtoupper($t->to))->lower(); 51 | $tell->chan = $t->chan; 52 | $tell->network = $network; 53 | if($t->to_net == null || $t->to_net == "") 54 | $tell->global = true; 55 | $cnt++; 56 | $entityManager->persist($tell); 57 | } 58 | 59 | $entityManager->flush(); 60 | 61 | echo "Skipped tells from networks: " . implode(', ', $otherNets) . "\n"; 62 | echo "$cnt Tells imported\n"; -------------------------------------------------------------------------------- /import_old_db/weather.php: -------------------------------------------------------------------------------- 1 | \n"); 17 | } 18 | 19 | $network = $entityManager->getRepository(Network::class)->find($network_id); 20 | if(!$network) 21 | die("Couldn't find that network_id\n"); 22 | 23 | $locs = unserialize(file_get_contents($dbfile)); 24 | 25 | foreach($locs as $nick => $l) { 26 | $location = new \scripts\weather\entities\location(); 27 | $location->nick = u(strtoupper($nick))->lower(); 28 | $location->si = $l['si']; 29 | $location->name = $l['location']; 30 | $location->lat = $l['lat']; 31 | $location->long = $l['lon']; 32 | $location->network = $network; 33 | $entityManager->persist($location); 34 | } 35 | 36 | $entityManager->flush(); 37 | 38 | echo "Weather locations imported\n"; 39 | -------------------------------------------------------------------------------- /library/Duration.inc: -------------------------------------------------------------------------------- 1 | 12, 'minutes' => 1) 18 | * into '1 minute, 12 seconds'. 19 | * 20 | * This class is plural aware. Time segments with values 21 | * other than 1 will have an 's' appended. 22 | * For example, '1 second' not '1 seconds'. 23 | * 24 | * @author Aidan Lister 25 | * @version 1.2.1 26 | * @link http://aidanlister.com/repos/v/Duration.php 27 | */ 28 | 29 | 30 | /** 31 | * All in one method 32 | * 33 | * @param int|array $duration Array of time segments or a number of seconds 34 | * @return string 35 | */ 36 | function Duration_toString($duration, $periods = null) { 37 | if (!is_array($duration)) { 38 | $duration = Duration_int2array($duration, $periods); 39 | } 40 | 41 | return Duration_array2string($duration); 42 | } 43 | 44 | //After tons of calculations I've decided on these numbers -knivey 45 | $Duration_periods = array( 46 | 'y' => 31533336, 47 | 'M' => 2627778, 48 | 'w' => 604800, 49 | 'd' => 86400, 50 | 'h' => 3600, 51 | 'm' => 60, 52 | 's' => 1 53 | ); 54 | 55 | /** 56 | * Takes a string duration like srvx ex(1h30m) and turns it into int seconds 57 | * @param string $duration 58 | * @return int|string 59 | * @author knivey 60 | */ 61 | function string2Seconds(string $duration): int|string { 62 | global $Duration_periods; 63 | $string = trim($duration); 64 | $string = str_split($string); 65 | $num = 0; 66 | $total = 0; 67 | foreach ($string as $chunk) { 68 | if (is_numeric($chunk)) { 69 | $num .= $chunk; 70 | } else { 71 | if(array_key_exists($chunk, $Duration_periods)) { 72 | if($num == 0) { 73 | return "No value given for time identifier $chunk"; 74 | } 75 | $total += $num * $Duration_periods[$chunk]; 76 | } else { 77 | return "$chunk is an unknown time identifier"; 78 | } 79 | if($total > PHP_INT_MAX) { 80 | return "$duration is too great"; 81 | } 82 | if(!is_int($total)) { 83 | return "$duration not understood"; 84 | } 85 | $num = 0; 86 | } 87 | } 88 | return $total; 89 | } 90 | 91 | /** 92 | * Return an array of date segments. 93 | * 94 | * @param int $seconds Number of seconds to be parsed 95 | * @return mixed An array containing named segments 96 | */ 97 | function Duration_int2array($seconds, $periods = null) { 98 | global $Duration_periods; 99 | // Define time periods 100 | if (!is_array($periods)) { 101 | $periods = $Duration_periods; 102 | } 103 | 104 | // Loop 105 | $seconds = (float) $seconds; 106 | foreach ($periods as $period => $value) { 107 | $count = floor($seconds / $value); 108 | 109 | if ($count == 0) { 110 | continue; 111 | } 112 | 113 | $values[$period] = $count; 114 | $seconds = $seconds % $value; 115 | } 116 | 117 | // Return 118 | if (empty($values)) { 119 | $values = null; 120 | } 121 | 122 | return $values; 123 | } 124 | 125 | /** 126 | * Return a string of time periods. 127 | * 128 | * @package Duration 129 | * @param mixed $duration An array of named segments 130 | * @return string 131 | */ 132 | function Duration_array2string($duration, $nopad = false) { 133 | if (!is_array($duration)) { 134 | return false; 135 | } 136 | $array = []; 137 | foreach ($duration as $key => $value) { 138 | //$segment_name = substr($key, 0, -1); 139 | //$segment = $value . ' ' . $segment_name; 140 | $segment = $value . $key; 141 | // Plural 142 | //if ($value != 1) { 143 | // $segment .= 's'; 144 | //} 145 | 146 | $array[] = $segment; 147 | } 148 | 149 | if($nopad) { 150 | return implode('', $array); 151 | } 152 | return implode(', ', $array); 153 | 154 | } 155 | 156 | -------------------------------------------------------------------------------- /library/Irc/Consts.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | protected array $eventCallbacks = array(); 15 | /** 16 | * @var array> 17 | */ 18 | protected array $onceEventCallbacks = array(); 19 | 20 | /** 21 | * @param string $event 22 | * @param callable(object $event, T $eventEmitter): void $callback 23 | * @param int|null $idx 24 | * @return $this 25 | */ 26 | public function on(string $event, callable $callback, ?int &$idx = null): static 27 | { 28 | if (str_contains($event, ',')) { 29 | $events = explode(',', $event); 30 | foreach ($events as $event) { 31 | $this->on($event, $callback); 32 | } 33 | return $this; 34 | } 35 | 36 | $this->eventCallbacks[$event][] = $callback; 37 | $idx = array_key_last($this->eventCallbacks[$event]); 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param string $event 43 | * @param ?callable(object $event, T $eventEmitter): void $callback 44 | * @param ?int $idx 45 | * @return $this 46 | */ 47 | public function off(string $event, ?callable $callback, ?int $idx = null): static 48 | { 49 | if (!isset($this->eventCallbacks[$event]) || empty($this->eventCallbacks[$event]) ) 50 | return $this; 51 | 52 | if($idx === null) { 53 | foreach ($this->eventCallbacks[$event] as $key => $cb) 54 | if ($callback === $cb) { 55 | $idx = $key; 56 | break; 57 | } 58 | } 59 | if($idx === null) 60 | return $this; 61 | 62 | unset($this->eventCallbacks[$event][$idx]); 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param string $event 68 | * @param callable(object $event, T $eventEmitter): void $callback 69 | * @return $this 70 | */ 71 | public function once(string $event, callable $callback): static 72 | { 73 | $this->onceEventCallbacks[$event][] = $callback; 74 | return $this; 75 | } 76 | 77 | //TODO the object sent by this is stupid and dumb would like to make better defined objects 78 | 79 | public function emit(string $event, array $args = array()): static 80 | { 81 | if (str_contains($event, ',')) { 82 | $events = explode(',', $event); 83 | foreach ($events as $event) { 84 | $this->emit(trim($event), $args); 85 | } 86 | return $this; 87 | } 88 | 89 | $args['time'] = time(); 90 | $args['event'] = $event; 91 | $args['sender'] = $this; 92 | 93 | if (!empty($this->onceEventCallbacks[$event])) { 94 | foreach ($this->onceEventCallbacks[$event] as $callback) 95 | call_user_func($callback, (object)$args, $this); 96 | $this->onceEventCallbacks[$event] = array(); 97 | } 98 | 99 | if (!empty($this->eventCallbacks[$event])) { 100 | foreach ($this->eventCallbacks[$event] as $callback) 101 | call_user_func($callback, (object)$args, $this); 102 | } 103 | 104 | return $this; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /library/Irc/Exception.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public array $args = array(); 17 | 18 | /** 19 | * @param string $command 20 | * @param list $args 21 | * @param string|null $prefix 22 | */ 23 | public function __construct(string $command, array $args = array(), ?string $prefix = null) 24 | { 25 | $this->command = $command; 26 | $this->args = $args; 27 | 28 | if (!empty($prefix)) { 29 | if (str_contains($prefix, '!')) { 30 | $parts = preg_split('/[!@]/', $prefix); 31 | $this->nick = $parts[0]; 32 | $this->name = $parts[1] ?? ''; 33 | $this->ident = $this->name; 34 | $this->host = $parts[2] ?? ''; 35 | } else { 36 | $this->nick = $prefix; 37 | } 38 | } 39 | } 40 | 41 | public function __toString(): string 42 | { 43 | $args = array_map(strval(...), $this->args); 44 | $len = count($args); 45 | $last = $len - 1; 46 | 47 | if ($len > 0 && (str_contains($args[$last], ' ') || $args[$last][0] === ':')) { 48 | $args[$last] = ':' . $args[$last]; 49 | } 50 | 51 | $prefix = $this->getHostString(); 52 | 53 | array_unshift($args, $this->command); 54 | if ($prefix != '') 55 | array_unshift($args, ":$prefix"); 56 | 57 | return implode(' ', $args); 58 | } 59 | 60 | public function getHostString(): string 61 | { 62 | $str = "$this->nick"; 63 | 64 | if ($this->name !== null) 65 | $str .= "!$this->name"; 66 | 67 | if ($this->host !== null) 68 | $str .= "@$this->host"; 69 | 70 | return $str; 71 | } 72 | 73 | public function getIdentHost(): string { 74 | $str = ''; 75 | if($this->ident != null) 76 | $str .= "$this->ident@"; 77 | return "{$str}$this->host"; 78 | } 79 | 80 | /** 81 | * @param int $index 82 | * @param ?string $defaultValue 83 | * @return ($defaultValue is string ? string : ?string) 84 | */ 85 | public function getArg(int $index, ?string $defaultValue = null): ?string 86 | { 87 | return $this->args[$index] ?? $defaultValue; 88 | } 89 | 90 | public static function parse(string $message): ?Message 91 | { 92 | if (empty($message)) 93 | return null; 94 | 95 | $args = array(); 96 | $matches = array(); 97 | 98 | if (preg_match('/^ 99 | (:(?[^ ]+)\s+)? #the prefix (either "server" or "nick!user@host") 100 | (?[^ ]+) #the command (e.g. NOTICE, PRIVMSG) 101 | (?.*) #The argument string 102 | $/x', $message, $matches)) { 103 | 104 | $prefix = $matches['prefix'] ?? ''; 105 | $command = $matches['command'] ?? ''; 106 | 107 | $spacedArg = false; 108 | if (!empty($matches['args'])) { 109 | if (str_contains($matches['args'], ' :')) { 110 | $parts = explode(' :', $matches['args'], 2); 111 | $args = explode(' ', $parts[0]); 112 | $spacedArg = $parts[1]; 113 | } else if (str_starts_with($matches['args'], ':')) 114 | $spacedArg = substr($matches['args'], 1); 115 | else 116 | $args = explode(' ', $matches['args']); 117 | } 118 | 119 | $args = array_values(array_filter($args)); 120 | if($spacedArg !== false) 121 | $args[] = $spacedArg; 122 | } else 123 | return new Message('UNKNOWN', array($message)); 124 | 125 | return new Message($command, $args, $prefix); 126 | } 127 | } -------------------------------------------------------------------------------- /library/async_get_contents.php: -------------------------------------------------------------------------------- 1 | getMessage(); 10 | $out = str_replace(["\n", "\r"], " ", $out); 11 | $out = html_entity_decode($out, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 12 | $out = htmlspecialchars_decode($out); 13 | $out = str_replace("\x01", "[CTCP]", $out); 14 | return substr($out, 0, 200); 15 | } 16 | 17 | public function getIRCMsg(): string { 18 | return "Error ({$this->getCode()}): {$this->getMessageStripped()}"; 19 | } 20 | } 21 | 22 | /** 23 | * @param $url 24 | * @param array $headers Headers to override/set 25 | * @throws async_get_exception 26 | * @return string 27 | */ 28 | function async_get_contents(string $url, array $headers = []): string { 29 | $client = HttpClientBuilder::buildDefault(); 30 | $request = new Request($url); 31 | foreach ($headers as $header => $value) 32 | $request->setHeader($header, $value); 33 | /** @var Response $response */ 34 | $response = $client->request($request); 35 | $body = $response->getBody()->buffer(); 36 | if ($response->getStatus() != 200) { 37 | throw new async_get_exception($body, $response->getStatus()); 38 | } 39 | return $body; 40 | } -------------------------------------------------------------------------------- /library/draw/Color.php: -------------------------------------------------------------------------------- 1 | fg; 46 | } 47 | if ($name == 'bg') { 48 | return $this->bg; 49 | } 50 | $trace = debug_backtrace(); 51 | trigger_error( 52 | 'Undefined property via __get(): ' . $name . 53 | ' in ' . $trace[0]['file'] . 54 | ' on line ' . $trace[0]['line'], 55 | E_USER_NOTICE 56 | ); 57 | return false; 58 | } 59 | 60 | public function equals(Color $color): bool 61 | { 62 | if ($this->fg === $color->fg && $this->bg === $color->bg) { 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | //thinking this can be like an array of colors with a step size? 69 | public function setGradiant(array $colors) 70 | { 71 | 72 | } 73 | 74 | public function advanceGradiant() 75 | { 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /library/draw/Pixel.php: -------------------------------------------------------------------------------- 1 | text; 15 | } 16 | } -------------------------------------------------------------------------------- /migrations.yml: -------------------------------------------------------------------------------- 1 | table_storage: 2 | table_name: doctrine_migration_versions 3 | version_column_name: version 4 | version_column_length: 1024 5 | executed_at_column_name: executed_at 6 | execution_time_column_name: execution_time 7 | 8 | migrations_paths: 9 | 'lolbot\Migrations': ./Migrations 10 | 11 | all_or_nothing: true 12 | transactional: true 13 | check_database_platform: true 14 | organize_migrations: none 15 | 16 | connection: null 17 | em: null 18 | -------------------------------------------------------------------------------- /multiartconfig.example.yaml: -------------------------------------------------------------------------------- 1 | #general options from artconfig.yaml may be used in here 2 | 3 | channels: ['#knivey'] 4 | trigger: "@" 5 | artdir: "/home/knivey/ircart/" 6 | bots: 7 | - 8 | name: van 9 | server: snek.irc.ltd 10 | port: 6667 11 | ssl: false 12 | throttle: false 13 | bindIp: 0 14 | - 15 | name: pee 16 | server: snek.irc.ltd 17 | port: 6667 18 | ssl: false 19 | throttle: false 20 | bindIp: 0 21 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | fileExtensions: 3 | - php 4 | - inc 5 | level: 5 6 | paths: 7 | - . 8 | excludePaths: 9 | analyse: 10 | - vendor 11 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /scripts/alias/entities/alias.php: -------------------------------------------------------------------------------- 1 | ...')] 14 | #[Desc("Search bing.com")] 15 | #[Option("--amt", "How many results to show")] 16 | #[Option("--result", "Show result at this position")] 17 | function bing($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 18 | { 19 | global $config, $warned, $ratelimit; 20 | if(!isset($config['bingKey'])) { 21 | echo "bingKey not set in config\n"; 22 | return; 23 | } 24 | if(!isset($config['bingEP'])) { 25 | echo "bingKbingEPey not set in config\n"; 26 | return; 27 | } 28 | if(!isset($config['bingLang'])) { 29 | echo "bingLang not set in config\n"; 30 | return; 31 | } 32 | 33 | if (time() < $ratelimit) { 34 | if(!$warned) 35 | $bot->pm($args->chan, "Whoa slow down!"); 36 | $warned = true; 37 | return; 38 | } 39 | $warned = false; 40 | $ratelimit = time() + 2; 41 | 42 | $start = 1; 43 | $end = 1; 44 | if($cmdArgs->optEnabled("--amt")) { 45 | $end = $cmdArgs->getOpt("--amt"); 46 | if($end < 1 || $end > 4) { 47 | $bot->pm($args->chan, "\2Bing:\2 --amt should be from 1 to 4"); 48 | return; 49 | } 50 | } 51 | if($cmdArgs->optEnabled("--result")) { 52 | $start = $cmdArgs->getOpt("--result"); 53 | if($start < 1 || $start > 11) { //default 10 returned 54 | $bot->pm($args->chan, "\2Bing:\2 --result should be from 1 to 10"); 55 | return; 56 | } 57 | $end = $start; 58 | } 59 | $query = urlencode($cmdArgs['query']); 60 | $url = $config['bingEP'] . "search?q=$query&mkt=$config[bingLang]&setLang=$config[bingLang]"; 61 | try { 62 | $headers = ['Ocp-Apim-Subscription-Key' => $config['bingKey']]; 63 | $body = async_get_contents($url, $headers); 64 | $j = json_decode($body, true); 65 | 66 | if (!array_key_exists('webPages', $j)) { 67 | $bot->pm($args->chan, "\2Bing:\2 No Results"); 68 | return; 69 | } 70 | $results = number_format($j['webPages']['totalEstimatedMatches']); 71 | 72 | for ($i = ($start - 1); $i <= $end-1; $i++) { 73 | if(!isset($j['webPages']['value'][$i])) { 74 | $bot->pm($args->chan, "End of results :("); 75 | break; 76 | } 77 | $res = $j['webPages']['value'][$i]; 78 | $bot->pm($args->chan, "\2Bing (\2$results Results\2):\2 $res[url] ($res[name]) -- $res[snippet]"); 79 | } 80 | } catch (\async_get_exception $error) { 81 | echo $error; 82 | $bot->pm($args->chan, "\2Bing:\2 {$error->getIRCMsg()}"); 83 | } catch (\Exception $error) { 84 | echo $error->getMessage(); 85 | $bot->pm($args->chan, "\2Bing:\2 {$error->getMessage()}"); 86 | } 87 | } -------------------------------------------------------------------------------- /scripts/bomb_game/bomb.php: -------------------------------------------------------------------------------- 1 | ...')] 14 | #[Desc("Search brave.com")] 15 | #[Option("--amt", "How many results to show")] 16 | function brave($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 17 | { 18 | global $config, $warned, $ratelimit; 19 | if (!isset($config['braveKey'])) { 20 | echo "braveKey not set in config\n"; 21 | return; 22 | } 23 | 24 | if (time() < $ratelimit) { 25 | if (!$warned) 26 | $bot->pm($args->chan, "Whoa slow down!"); 27 | $warned = true; 28 | return; 29 | } 30 | $warned = false; 31 | $ratelimit = time() + 2; 32 | 33 | $query = urlencode($cmdArgs['query']); 34 | $url = "https://api.search.brave.com/res/v1/web/search?q=$query&country=US&safesearch=off&spellcheck=false&result_filter=web"; 35 | try { 36 | $headers = ['X-Subscription-Token' => $config['braveKey']]; 37 | $body = async_get_contents($url, $headers); 38 | $j = json_decode($body); 39 | 40 | if (!isset($j->web->results) || count($j->web->results) < 1) { 41 | $bot->pm($args->chan, "\2Brave:\2 No Results"); 42 | return; 43 | } 44 | $end = 1; 45 | if($cmdArgs->optEnabled("--amt")) { 46 | $end = $cmdArgs->getOpt("--amt"); 47 | if($end < 1 || $end > 10) { 48 | $bot->pm($args->chan, "\2Brave:\2 --amt should be from 1 to 10"); 49 | return; 50 | } 51 | } 52 | 53 | for ($i = 0; $i <= $end-1; $i++) { 54 | if(!isset($j->web->results[$i])) { 55 | $bot->pm($args->chan, "End of results :("); 56 | break; 57 | } 58 | $res = $j->web->results[$i]; 59 | $desc = str_replace(["", ""], "\2", $res->description); 60 | $desc = html_entity_decode($desc, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 61 | $desc = htmlspecialchars_decode($desc); 62 | $bot->pm($args->chan, "\2Brave web search:\2 $res->url $res->title -- $desc"); 63 | } 64 | } catch (\async_get_exception $error) { 65 | echo $error; 66 | $bot->pm($args->chan, "\2Brave:\2 {$error->getIRCMsg()}"); 67 | } catch (\Exception $error) { 68 | echo $error->getMessage(); 69 | $bot->pm($args->chan, "\2Brave:\2 {$error->getMessage()}"); 70 | } 71 | } -------------------------------------------------------------------------------- /scripts/codesand/config.example.yaml: -------------------------------------------------------------------------------- 1 | server: "http://localhost:1337" 2 | key: key 3 | -------------------------------------------------------------------------------- /scripts/durendaltv/durendaltv.php: -------------------------------------------------------------------------------- 1 | pm($args->chan, "https://live.internetrelaychat.net - stream is offline"); 15 | return; 16 | } 17 | 18 | try { 19 | $kvdb_url = "http://live-kvdb.florp.us"; 20 | //$data = async_get_contents("https://kvdb.io/4gam6KTdmSsvkDFZxFaUHz/now_playing"); 21 | $data = async_get_contents($kvdb_url); 22 | $bot->pm($args->chan, "https://live.internetrelaychat.net - $data (mpv https://live.internetrelaychat.net/live/stream.m3u8)"); 23 | } catch (\Exception $e) { 24 | $bot->pm($args->chan, "Error fetching data from $kvdb_url"); 25 | echo $e->getMessage(); 26 | $bot->pm($args->chan, "https://live.internetrelaychat.net - stream up but error getting now playing (mpv https://live.internetrelaychat.net/live/stream.m3u8)"); 27 | } 28 | } -------------------------------------------------------------------------------- /scripts/help/help.php: -------------------------------------------------------------------------------- 1 | optEnabled("--priv")) { 20 | if (!$this->router->cmdExists($cmdArgs['command'])) { 21 | $bot->msg($args->chan, "no command by that name"); 22 | return; 23 | } 24 | $help = (string)$this->router->cmds[$cmdArgs['command']]; 25 | } else { 26 | if (!$this->router->cmdExistsPriv($cmdArgs['command'])) { 27 | $bot->msg($args->chan, "no command by that name"); 28 | return; 29 | } 30 | $help = (string)$this->router->privCmds[$cmdArgs['command']]; 31 | } 32 | $this->showHelp($args->chan, $bot, $help); 33 | return; 34 | } 35 | $first = 0; 36 | if ($cmdArgs->optEnabled("--priv")) 37 | $cmds = $this->router->privCmds; 38 | else 39 | $cmds = $this->router->cmds; 40 | $out = ""; 41 | foreach ($cmds as $cmd) { 42 | if ($first++) 43 | $out .= "---\n"; 44 | $out .= (string)$cmd . "\n"; 45 | } 46 | $this->showHelp($args->chan, $bot, $out); 47 | } 48 | 49 | function showHelp(string $chan, $bot, string $lines) 50 | { 51 | if (function_exists('pumpToChan')) { 52 | pumpToChan($chan, explode("\n", $lines)); 53 | return; 54 | } 55 | if (strlen($lines) > 1000) { 56 | try { 57 | $connectContext = (new \Amp\Socket\ConnectContext) 58 | ->withConnectTimeout(250); 59 | $sock = \Amp\Socket\connect("tcp://termbin.com:9999", $connectContext); 60 | $sock->write($lines); 61 | $url = \Amp\ByteStream\buffer($sock); 62 | $sock->end(); 63 | $bot->msg($chan, "help: $url"); 64 | } catch (\Exception $e) { 65 | $bot->msg($chan, "trouble uploading help :("); 66 | } 67 | return; 68 | } 69 | foreach (explode("\n", $lines) as $line) { 70 | $bot->msg($chan, $line); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /scripts/invidious/invidious.php: -------------------------------------------------------------------------------- 1 | addListener($this->handleEvents(...)); 16 | } 17 | 18 | function handleEvents(UrlEvent $event): void 19 | { 20 | if ($event->handled) 21 | return; 22 | if (!preg_match("@^https?://[^/]+/watch\?v=.*$@i", $event->url, $m)) 23 | return; 24 | $URL = '@^((?:https?:)?//)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(/(?:[\w\-]+\?v=|shorts/|embed/|v/)?)([\w\-]+)(\S+)?$@i'; 25 | if (preg_match($URL, $event->url, $m)) { 26 | return; 27 | } 28 | 29 | $event->addFuture(\Amp\async(function () use ($event) { 30 | try { 31 | $client = HttpClientBuilder::buildDefault(); 32 | $req = new Request($event->url); 33 | $response = $client->request($req); 34 | $body = $response->getBody()->buffer(); 35 | if ($response->getStatus() != 200) { 36 | echo "invidious lookup failed with code {$response->getStatus()}\n"; 37 | var_dump($body); 38 | return; 39 | } 40 | 41 | $html = new HtmlDocument(); 42 | $html->load($body); 43 | $check = $html->find("meta [property=\"og:site_name\"]", 0)?->attr['content'] ?? ""; 44 | if (!preg_match("/^.* \| Invidious$/", $check)) { 45 | echo "url didnt pass invidious check\n"; 46 | return; 47 | } 48 | $title = $html->find("meta [property=\"og:title\"]", 0)?->attr['content'] ?? "?"; 49 | //$views = $html->find("p#views", 0)?->plaintext ?? "?"; 50 | $channel = $html->find("span#channel-name", 0)?->plaintext ?? "?"; 51 | $date = $html->find("p#published-date", 0)?->plaintext ?? "?"; 52 | $vd = $html->find("script#video_data", 0)?->innertext; 53 | if (!is_string($vd)) { 54 | echo "invidious didnt get video data json, aborting\n"; 55 | return; 56 | } 57 | $json = json_decode($vd, flags: JSON_THROW_ON_ERROR); 58 | $length = \Duration_toString($json?->length_seconds ?? 0); 59 | $rpl = "\x0315,01[\x0300,01I\x0315ꞐꝞI\x0314D\x0300IꝊU\x0315Ꞩ\x0300,01]\x03 $title | $channel | $date | $length"; 60 | $event->reply($rpl); 61 | } catch (\Exception $e) { 62 | echo "invidious exception {$e->getMessage()}\n"; 63 | return; 64 | } 65 | })); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/lastfm/entities/lastfm.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public array $futures = []; 18 | /** 19 | * @var list 20 | */ 21 | public array $replies = []; 22 | 23 | /** 24 | * 25 | * @param Future $future 26 | * @return void 27 | */ 28 | public function addFuture(Future $future) { 29 | $this->futures[] = $future; 30 | } 31 | 32 | public function awaitAll() { 33 | return Future\awaitAll($this->futures); 34 | } 35 | 36 | public function reply(string $msg) { 37 | $this->handled = true; 38 | $this->replies[] = $msg; 39 | } 40 | 41 | public function sendReplies($bot, $chan) { 42 | foreach ($this->replies as $msg) 43 | $bot->pm($chan, " $msg"); 44 | } 45 | 46 | public function doLog($linktitles, $bot) { 47 | $linktitles->logUrl($bot, $this->nick, $this->chan, $this->text, $this->replies); 48 | } 49 | } -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/hostignore_add.php: -------------------------------------------------------------------------------- 1 | addArgument("type", InputArgument::REQUIRED); 27 | $this->addArgument("hostmask", InputArgument::REQUIRED); 28 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Network ID"); 29 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Bot ID"); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int { 33 | global $entityManager; 34 | try { 35 | $type = ignore_type::fromString($input->getArgument("type")); 36 | } catch (\UnhandledMatchError) { 37 | throw new \InvalidArgumentException("Type must be one of: global, network, bot, channel"); 38 | } 39 | 40 | $ignore = new hostignore($type); 41 | $ignore->hostmask = $input->getArgument("hostmask"); 42 | 43 | switch($type) { 44 | case ignore_type::global: 45 | break; 46 | case ignore_type::network: 47 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 48 | if($network == null) { 49 | throw new \InvalidArgumentException("Couldn't find that network ID"); 50 | } 51 | $ignore->network = $network; 52 | break; 53 | case ignore_type::bot: 54 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 55 | if($bot == null) { 56 | throw new \InvalidArgumentException("Couldn't find that network ID"); 57 | } 58 | $ignore->bot = $bot; 59 | break; 60 | case ignore_type::channel: 61 | throw new \Exception('Channel type not implemented yet'); 62 | } 63 | 64 | $entityManager->persist($ignore); 65 | $entityManager->flush(); 66 | 67 | $output->writeln("Host Ignore added"); 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/hostignore_del.php: -------------------------------------------------------------------------------- 1 | addArgument("id", InputArgument::REQUIRED); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | global $entityManager; 28 | $ignore = $entityManager->getRepository(hostignore::class)->find($input->getArgument("id")); 29 | if($ignore == null) { 30 | throw new \InvalidArgumentException("Couldn't find a hostignore by that ID"); 31 | } 32 | 33 | $entityManager->remove($ignore); 34 | $entityManager->flush(); 35 | 36 | $output->writeln("Host Ignore removed"); 37 | 38 | return Command::SUCCESS; 39 | } 40 | } -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/hostignore_list.php: -------------------------------------------------------------------------------- 1 | addOption("type", "t", InputOption::VALUE_REQUIRED, "Filter by type (global, network, bot, channel)"); 28 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Filter by network ID"); 29 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Filter by bot ID"); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | global $entityManager; 35 | $ignores = $entityManager->getRepository(hostignore::class); 36 | $criteria = Criteria::create(); 37 | if($input->getOption("type") !== null) { 38 | try { 39 | $type = ignore_type::fromString($input->getOption("type")); 40 | } catch (\UnhandledMatchError) { 41 | throw new \InvalidArgumentException("Type must be one of: global, network, bot, channel"); 42 | } 43 | $criteria->andWhere(Criteria::expr()->eq("type", $type)); 44 | } 45 | if($input->getOption("network") !== null) { 46 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 47 | if($network === null) { 48 | throw new \InvalidArgumentException("Couldn't find that network ID"); 49 | } 50 | $criteria->andWhere(Criteria::expr()->eq("network", $network)); 51 | } 52 | if($input->getOption("bot") !== null) { 53 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 54 | if($bot === null) { 55 | throw new \InvalidArgumentException("Couldn't find that network ID"); 56 | } 57 | $criteria->andWhere(Criteria::expr()->eq("bot", $bot)); 58 | } 59 | 60 | $ignores = $ignores->matching($criteria)->toArray(); 61 | 62 | $io = new SymfonyStyle($input, $output); 63 | $table_head = ["id","hostmask","type","network","bot","chan","created"]; 64 | $table = []; 65 | foreach($ignores as $ignore) { 66 | $table[] = [$ignore->id, $ignore->hostmask, $ignore->type->toString(), $ignore->network?->id, $ignore->bot?->id, "", $ignore->created->format('r')]; 67 | } 68 | $io->table($table_head, $table); 69 | 70 | return Command::SUCCESS; 71 | } 72 | } 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/hostignore_test.php: -------------------------------------------------------------------------------- 1 | addArgument("host", InputArgument::REQUIRED, "The host to test for ignores"); 28 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Filter by network ID"); 29 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Filter by bot ID"); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | global $entityManager; 35 | $ignores = $entityManager->getRepository(hostignore::class); 36 | $criteria = Criteria::create(); 37 | if($input->getOption("network") !== null) { 38 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 39 | if($network === null) { 40 | throw new \InvalidArgumentException("Couldn't find that network ID"); 41 | } 42 | $criteria->andWhere(Criteria::expr()->eq("network", $network)); 43 | } 44 | if($input->getOption("bot") !== null) { 45 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 46 | if($bot === null) { 47 | throw new \InvalidArgumentException("Couldn't find that network ID"); 48 | } 49 | $criteria->andWhere(Criteria::expr()->eq("bot", $bot)); 50 | } 51 | 52 | $ignores = $ignores->matching($criteria)->toArray(); 53 | 54 | $ignores = array_filter($ignores, function($ignore) use ($input) { 55 | if (preg_match(\knivey\tools\globToRegex($ignore->hostmask) . 'i', $input->getArgument("host"))) { 56 | return true; 57 | } 58 | return false; 59 | }); 60 | 61 | $io = new SymfonyStyle($input, $output); 62 | $table_head = ["id","hostmask","type","network","bot","chan","created"]; 63 | $table = []; 64 | foreach($ignores as $ignore) { 65 | $table[] = [$ignore->id, $ignore->hostmask, $ignore->type->toString(), $ignore->network?->id, $ignore->bot?->id, "", $ignore->created->format('r')]; 66 | } 67 | $io->table($table_head, $table); 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/ignore_add.php: -------------------------------------------------------------------------------- 1 | addArgument("type", InputArgument::REQUIRED); 27 | $this->addArgument("regex", InputArgument::REQUIRED); 28 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Network ID"); 29 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Bot ID"); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int { 33 | global $entityManager; 34 | try { 35 | $type = ignore_type::fromString($input->getArgument("type")); 36 | } catch (\UnhandledMatchError) { 37 | throw new \InvalidArgumentException("Type must be one of: global, network, bot, channel"); 38 | } 39 | 40 | $re = "@{$input->getArgument("regex")}@i"; 41 | if(preg_match($re, "") === false) { 42 | throw new \InvalidArgumentException("You haven't provided a valid regex, the delimeter @ is added for you"); 43 | } 44 | 45 | $ignore = new ignore($type); 46 | $ignore->regex = $re; 47 | 48 | switch($type) { 49 | case ignore_type::global: 50 | break; 51 | case ignore_type::network: 52 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 53 | if($network == null) { 54 | throw new \InvalidArgumentException("Couldn't find that network ID"); 55 | } 56 | $ignore->network = $network; 57 | break; 58 | case ignore_type::bot: 59 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 60 | if($bot == null) { 61 | throw new \InvalidArgumentException("Couldn't find that network ID"); 62 | } 63 | $ignore->bot = $bot; 64 | break; 65 | case ignore_type::channel: 66 | throw new \Exception('Channel type not implemented yet'); 67 | } 68 | 69 | $entityManager->persist($ignore); 70 | $entityManager->flush(); 71 | 72 | $output->writeln("Ignore added"); 73 | 74 | return Command::SUCCESS; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/ignore_del.php: -------------------------------------------------------------------------------- 1 | addArgument("id", InputArgument::REQUIRED); 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | global $entityManager; 28 | $ignore = $entityManager->getRepository(ignore::class)->find($input->getArgument("id")); 29 | if($ignore == null) { 30 | throw new \InvalidArgumentException("Couldn't find an ignore by that ID"); 31 | } 32 | 33 | $entityManager->remove($ignore); 34 | $entityManager->flush(); 35 | 36 | $output->writeln("Ignore removed"); 37 | 38 | return Command::SUCCESS; 39 | } 40 | } -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/ignore_list.php: -------------------------------------------------------------------------------- 1 | addOption("type", "t", InputOption::VALUE_REQUIRED, "Filter by type (global, network, bot, channel)"); 29 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Filter by network ID"); 30 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Filter by bot ID"); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output): int 34 | { 35 | global $entityManager; 36 | $ignores = $entityManager->getRepository(ignore::class); 37 | $criteria = Criteria::create(); 38 | if($input->getOption("type") !== null) { 39 | try { 40 | $type = ignore_type::fromString($input->getOption("type")); 41 | } catch (\UnhandledMatchError) { 42 | throw new \InvalidArgumentException("Type must be one of: global, network, bot, channel"); 43 | } 44 | $criteria->andWhere(Criteria::expr()->eq("type", $type)); 45 | } 46 | if($input->getOption("network") !== null) { 47 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 48 | if($network === null) { 49 | throw new \InvalidArgumentException("Couldn't find that network ID"); 50 | } 51 | $criteria->andWhere(Criteria::expr()->eq("network", $network)); 52 | } 53 | if($input->getOption("bot") !== null) { 54 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 55 | if($bot === null) { 56 | throw new \InvalidArgumentException("Couldn't find that network ID"); 57 | } 58 | $criteria->andWhere(Criteria::expr()->eq("bot", $bot)); 59 | } 60 | 61 | $ignores = $ignores->matching($criteria); 62 | 63 | $io = new SymfonyStyle($input, $output); 64 | $table_head = ["id","regex","type","network","bot","chan","created"]; 65 | $table = []; 66 | foreach($ignores as $ignore) { 67 | $table[] = [$ignore->id, $ignore->regex, $ignore->type->toString(), $ignore->network?->id, $ignore->bot?->id, "", $ignore->created->format('r')]; 68 | } 69 | $io->table($table_head, $table); 70 | 71 | return Command::SUCCESS; 72 | } 73 | } 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /scripts/linktitles/cli_cmds/ignore_test.php: -------------------------------------------------------------------------------- 1 | addArgument("url", InputArgument::REQUIRED, "The URL to test for ignores"); 28 | $this->addOption("network", "N", InputOption::VALUE_REQUIRED, "Filter by network ID"); 29 | $this->addOption("bot", "B", InputOption::VALUE_REQUIRED, "Filter by bot ID"); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | global $entityManager; 35 | $ignores = $entityManager->getRepository(ignore::class); 36 | $criteria = Criteria::create(); 37 | if($input->getOption("network") !== null) { 38 | $network = $entityManager->getRepository(Network::class)->find($input->getOption("network")); 39 | if($network === null) { 40 | throw new \InvalidArgumentException("Couldn't find that network ID"); 41 | } 42 | $criteria->andWhere(Criteria::expr()->eq("network", $network)); 43 | } 44 | if($input->getOption("bot") !== null) { 45 | $bot = $entityManager->getRepository(Bot::class)->find($input->getOption("bot")); 46 | if($bot === null) { 47 | throw new \InvalidArgumentException("Couldn't find that network ID"); 48 | } 49 | $criteria->andWhere(Criteria::expr()->eq("bot", $bot)); 50 | } 51 | 52 | $ignores = $ignores->matching($criteria)->toArray(); 53 | 54 | $ignores = array_filter($ignores, function($ignore) use ($input) { 55 | if (preg_match($ignore->regex, $input->getArgument("url"))) { 56 | return true; 57 | } 58 | return false; 59 | }); 60 | 61 | $io = new SymfonyStyle($input, $output); 62 | $table_head = ["id","regex","type","network","bot","chan","created"]; 63 | $table = []; 64 | foreach($ignores as $ignore) { 65 | $table[] = [$ignore->id, $ignore->regex, $ignore->type->toString(), $ignore->network?->id, $ignore->bot?->id, "", $ignore->created->format('r')]; 66 | } 67 | $io->table($table_head, $table); 68 | 69 | return Command::SUCCESS; 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /scripts/linktitles/entities/hostignore.php: -------------------------------------------------------------------------------- 1 | type = $type; 37 | $this->created = new \DateTimeImmutable(); 38 | } 39 | 40 | public function __toString():string { 41 | return "id: {$this->id} regex: {$this->hostmask} created: ".$this->created->format('r'); 42 | } 43 | } -------------------------------------------------------------------------------- /scripts/linktitles/entities/ignore.php: -------------------------------------------------------------------------------- 1 | type = $type; 41 | $this->created = new \DateTimeImmutable(); 42 | } 43 | 44 | public function __toString():string { 45 | return "id: {$this->id} regex: {$this->regex} type: {$this->type->toString()} created: ".$this->created->format('r'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/linktitles/entities/ignore_type.php: -------------------------------------------------------------------------------- 1 | self::global, 16 | "network" => self::network, 17 | "bot" => self::bot, 18 | "channel", "chan" => self::channel 19 | }; 20 | } 21 | 22 | public function toString(): string 23 | { 24 | return match($this) { 25 | self::global => "global", 26 | self::network => "network", 27 | self::bot => "bot", 28 | self::channel => "channel" 29 | }; 30 | } 31 | } -------------------------------------------------------------------------------- /scripts/notifier/notifier.php: -------------------------------------------------------------------------------- 1 | withTlsContext((new Socket\ServerTlsContext)->withDefaultCertificate($cert)); 17 | $server = SocketHttpServer::createForDirectAccess($logger); 18 | if(is_array($addresses)) { 19 | foreach ($addresses as $address) { 20 | $server->expose($address); 21 | } 22 | } else { 23 | $server->expose($addresses); 24 | } 25 | 26 | $errorHandler = new DefaultErrorHandler(); 27 | 28 | $server->start(new ClosureRequestHandler(function (Request $request) use (&$bot) { 29 | return requestHandler($request, $bot); 30 | }), $errorHandler); 31 | 32 | return $server; 33 | } 34 | 35 | function requestHandler(Request $request, $bot) { 36 | $path = $request->getUri()->getPath(); 37 | $path = explode('/', $path); 38 | $path = array_filter($path); 39 | 40 | $action = array_shift($path); 41 | 42 | //TODO might setup an actual router here if more scripts will have webhooks etc 43 | if (strtolower($action) == 'owncast') { 44 | $chan = array_shift($path); 45 | if(!$chan) { 46 | return new Response(HttpStatus::BAD_REQUEST, [ 47 | "content-type" => "text/plain; charset=utf-8" 48 | ], "Must specify a chan to privmsg"); 49 | } 50 | $body = $request->getBody()->buffer(); 51 | $json = json_decode($body, (bool)JSON_OBJECT_AS_ARRAY); 52 | //var_dump($json); 53 | //var_dump($body); 54 | if($json['type'] == 'STREAM_STARTED') 55 | $bot->pm("#$chan", "{$json['eventData']['name']} now streaming: {$json['eventData']['streamTitle']} | {$json['eventData']['summary']}"); 56 | if($json['type'] == 'STREAM_STOPPED') 57 | $bot->pm("#$chan", "{$json['eventData']['name']} stream stopped"); 58 | return new Response(HttpStatus::OK, [ 59 | "content-type" => "text/plain; charset=utf-8" 60 | ], "PRIVMSG sent"); 61 | } 62 | 63 | $notifier_keys = Yaml::parseFile(__DIR__. '/notifier_keys.yaml'); 64 | $key = $request->getHeader('key'); 65 | if (isset($notifier_keys[$key])) { 66 | echo "Request from {$notifier_keys[$key]} ($key)\n"; 67 | } else { 68 | return new Response(HttpStatus::FORBIDDEN, [ 69 | "content-type" => "text/plain; charset=utf-8" 70 | ], "Invalid key"); 71 | } 72 | if (strtolower($action) == 'privmsg') { 73 | $chan = array_shift($path); 74 | if(!$chan) { 75 | return new Response(HttpStatus::BAD_REQUEST, [ 76 | "content-type" => "text/plain; charset=utf-8" 77 | ], "Must specify a chan to privmsg"); 78 | } 79 | $msg = $request->getBody()->buffer(); 80 | $msg = str_replace("\r", "\n", $msg); 81 | $msg = explode("\n", $msg); 82 | foreach($msg as $line) { 83 | $bot->pm("#$chan", substr($line, 0, 400)); 84 | } 85 | return new Response(HttpStatus::OK, [ 86 | "content-type" => "text/plain; charset=utf-8" 87 | ], "PRIVMSG sent"); 88 | } 89 | 90 | return new Response(HttpStatus::BAD_REQUEST, [ 91 | "content-type" => "text/plain; charset=utf-8" 92 | ], "Unknown request"); 93 | } -------------------------------------------------------------------------------- /scripts/notifier/notifier_keys.example.yaml: -------------------------------------------------------------------------------- 1 | #rename this file to notifier_keys.yaml 2 | keyhere: keyname 3 | -------------------------------------------------------------------------------- /scripts/owncast/owncast.php: -------------------------------------------------------------------------------- 1 | chan, '#')); 24 | var_dump($chan); 25 | if(!isset($casts[$chan])) 26 | return; 27 | try { 28 | $url = "{$casts[$chan]}/api/status"; 29 | $surl = $casts[$chan]; 30 | $body = async_get_contents($url); 31 | $j = json_decode($body, true); 32 | if(!$j['online']) { 33 | $bot->pm($args->chan, "$surl - Stream is offline now."); 34 | return; 35 | } 36 | $line = "$surl "; 37 | //seems like stream proxied through nginx always shows no viewsers so just hide this part if 0 38 | if($j['viewerCount'] > 0) 39 | $line .= " {$j['viewerCount']} viewers, "; 40 | $line .= "streaming: {$j['streamTitle']}"; 41 | $bot->pm($args->chan, $line); 42 | } catch (\async_get_exception $e) { 43 | $bot->pm($args->chan, "owncast exception: {$e->getIRCMsg()}"); 44 | } catch (\Exception $e) { 45 | $bot->pm($args->chan, "owncast exception: {$e->getMessage()}"); 46 | } 47 | } -------------------------------------------------------------------------------- /scripts/reddit/reddit.php: -------------------------------------------------------------------------------- 1 | reddit = new \knivey\reddit\reddit($config['reddit'], "linux:lolbot:v1 (by /u/lolb0tlol)"); 21 | } 22 | 23 | public function setEventProvider(OrderedProviderInterface $eventProvider): void 24 | { 25 | $eventProvider->addListener($this->handleEvents(...)); 26 | } 27 | 28 | function handleEvents(UrlEvent $event) 29 | { 30 | global $config; 31 | if ($event->handled) 32 | return; 33 | if(!isset($config['reddit'])) 34 | return; 35 | 36 | // in the future we can probably handle all types from the info call and switch based on kind 37 | $SKIP_URLS = [ 38 | "@^https?://(?:www\.|old\.|pay\.|ssl\.|[a-z]{2}\.)?reddit\.com/r/([\w-]+)/?$@i", //subreddit 39 | "@^https?://(?:www\.|old\.|pay\.|ssl\.|[a-z]{2}\.)?reddit\.com/u(?:ser)?/([\w-]+)$@i", //user 40 | ]; 41 | $URLS = [ 42 | "@^https?://(redd\.it|reddit\.com)/(?P[\w-]+)/?$@i", //short post 43 | "@^https?://(?Pi|preview)\.redd\.it/(?P[^?\s]+)$@i", //image 44 | "@^https?://v\.redd\.it/([\w-]+)$@i", // video 45 | "@^https?://(?:www\.)?reddit\.com/gallery/([\w-]+)$@i" //gallery 46 | ]; 47 | foreach ($SKIP_URLS as $re) { 48 | if (preg_match($re, $event->url)) 49 | return; 50 | } 51 | $matched = false; 52 | foreach ($URLS as $re) { 53 | if (preg_match($re, $event->url)) { 54 | $matched = true; 55 | break; 56 | } 57 | } 58 | if (!$matched) 59 | return; 60 | 61 | $event->addFuture(\Amp\async(function () use ($event): void { 62 | try { 63 | $info = $this->reddit->info($event->url)->await(); 64 | if ($info->kind != 'Listing') { 65 | echo "reddit->info return was not kind: Listing\n"; 66 | return; 67 | } 68 | if (!isset($info->data->children) || !is_array($info->data->children)) { 69 | echo "reddit->info return didnt have data->children array\n"; 70 | return; 71 | } 72 | /* 73 | * t1_ Comment 74 | t2_ Account 75 | t3_ Link 76 | t4_ Message 77 | t5_ Subreddit 78 | t6_ Award 79 | */ 80 | $post = null; 81 | foreach ($info->data->children as $child) { 82 | if ($child->kind == "t3") { 83 | $post = $child->data ?? null; 84 | break; 85 | } 86 | } 87 | if ($post === null) { 88 | echo "reddit: no link kinds found in children\n"; 89 | return; 90 | } 91 | $date = Carbon::createFromTimestamp($post->created); 92 | $ago = $date->shortRelativeToNowDiffForHumans(null, 3); 93 | $ups = number_format($post->ups); 94 | $downs = number_format($post->downs); 95 | $reply = "[Reddit $post->subreddit_name_prefixed] $post->title (Posted $ago [+]{$ups} [-]$downs)"; 96 | $reply = html_entity_decode($reply, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 97 | $event->reply(str_replace(["\r", "\n"], " ", $reply)); 98 | } catch (\Exception $e) { 99 | echo "reddit exception {$e->getMessage()}\n"; 100 | return; 101 | } 102 | })); 103 | } 104 | } 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /scripts/remindme/entities/reminder.php: -------------------------------------------------------------------------------- 1 | init(); 27 | } 28 | 29 | public function init(): void {} 30 | } -------------------------------------------------------------------------------- /scripts/seen/entities/seen.php: -------------------------------------------------------------------------------- 1 | time = new \DateTime(); 40 | } 41 | } -------------------------------------------------------------------------------- /scripts/seen/seen.php: -------------------------------------------------------------------------------- 1 | ")] 22 | function seen($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 23 | { 24 | global $entityManager; 25 | $nick = u($cmdArgs['nick'])->lower(); 26 | if ($nick == u($bot->getNick())->lower()) { 27 | $bot->pm($args->chan, "I'm here bb"); 28 | return; 29 | } 30 | $this->saveSeens(); 31 | 32 | $seen = $entityManager->getRepository(entities\seen::class)->findOneBy([ 33 | "network" => $this->network, 34 | "nick" => $nick 35 | ]); 36 | if (!$seen) { 37 | $bot->pm($args->chan, "I've never seen {$cmdArgs['nick']} in my whole life"); 38 | return; 39 | } 40 | $entityManager->refresh($seen); 41 | try { 42 | $ago = (new Carbon($seen->time))->diffForHumans(Carbon::now(), CarbonInterface::DIFF_RELATIVE_TO_NOW, true, 3); 43 | } catch (\Exception $e) { 44 | echo $e->getMessage(); 45 | $ago = "??? ago"; 46 | } 47 | if ($args->chan != $seen->chan) { 48 | $bot->pm($args->chan, "{$seen->orig_nick} was last active in another channel $ago"); 49 | return; 50 | } 51 | $n = "<{$seen->orig_nick}>"; 52 | if ($seen->action == "action") { 53 | $n = "* {$seen->orig_nick}"; 54 | } 55 | if ($seen->action == "notice") { 56 | $n = "[{$seen->orig_nick}]"; 57 | } 58 | if(is_string($seen->text)) 59 | $text = $seen->text; 60 | else 61 | $text = stream_get_contents($seen->text); 62 | $bot->pm($args->chan, "seen {$ago}: $n {$text}"); 63 | } 64 | 65 | 66 | 67 | private $updates = []; 68 | 69 | function updateSeen(string $action, string $chan, string $nick, string $text) 70 | { 71 | $orig_nick = $nick; 72 | $nick = strtolower($nick); 73 | $chan = strtolower($chan); 74 | 75 | $ent = new entities\seen(); 76 | $ent->nick = $nick; 77 | $ent->orig_nick = $orig_nick; 78 | $ent->chan = $chan; 79 | $ent->text = $text; 80 | $ent->action = $action; 81 | $ent->network = $this->network; 82 | //Don't save yet, massive floods will destroy us with so many writes 83 | $this->updates[$nick] = $ent; 84 | } 85 | 86 | function saveSeens(): void 87 | { 88 | global $entityManager; 89 | foreach ($this->updates as $ent) { 90 | $previous = $entityManager->getRepository(entities\seen::class)->findOneBy([ 91 | "network" => $this->network, 92 | "nick" => $ent->nick 93 | ]); 94 | if ($previous) { 95 | $previous->nick = $ent->nick; 96 | $previous->orig_nick = $ent->orig_nick; 97 | $previous->chan = $ent->chan; 98 | $previous->text = $ent->text; 99 | $previous->action = $ent->action; 100 | $previous->time = $ent->time; 101 | $entityManager->persist($previous); 102 | } else { 103 | $entityManager->persist($ent); 104 | } 105 | } 106 | $entityManager->flush(); 107 | $this->updates = []; 108 | } 109 | 110 | function init(): void 111 | { 112 | \Revolt\EventLoop::repeat(15, $this->saveSeens(...)); 113 | 114 | $this->client->on('notice', function ($args, \Irc\Client $bot) { 115 | if (!$bot->isChannel($args->to)) 116 | return; 117 | //ignore ctcp replies to channel 118 | if (preg_match("@^\x01.*$@i", $args->text)) 119 | return; 120 | $this->updateSeen('notice', $args->to, $args->from, $args->text); 121 | }); 122 | 123 | $this->client->on('chat', function ($args, \Irc\Client $bot) { 124 | if (preg_match("@^\x01ACTION ([^\x01]+)\x01?$@i", $args->text, $m)) { 125 | $this->updateSeen('action', $args->chan, $args->from, $m[1]); 126 | return; 127 | } 128 | $this->updateSeen('privmsg', $args->chan, $args->from, $args->text); 129 | }); 130 | } 131 | } -------------------------------------------------------------------------------- /scripts/stocks/quote.php: -------------------------------------------------------------------------------- 1 | changePercent) && isset($this->change) && isset($this->price); 37 | } 38 | } -------------------------------------------------------------------------------- /scripts/stocks/symbol.php: -------------------------------------------------------------------------------- 1 | symbol) && 18 | isset($this->description) && 19 | isset($this->type); 20 | } 21 | } -------------------------------------------------------------------------------- /scripts/tell/entities/tell.php: -------------------------------------------------------------------------------- 1 | created = new \DateTime(); 43 | } 44 | } -------------------------------------------------------------------------------- /scripts/tiktok/tiktok.php: -------------------------------------------------------------------------------- 1 | addListener($this->handleEvents(...)); 17 | } 18 | 19 | function handleEvents(UrlEvent $event) 20 | { 21 | global $config; 22 | if ($event->handled) 23 | return; 24 | 25 | if (!preg_match("@^https?://(?:www\.)?tiktok\.com/\@[^/]+/[^/]+\??.*$@i", $event->url)) { 26 | return; 27 | } 28 | 29 | $event->addFuture(\Amp\async(function () use ($event): void { 30 | try { 31 | $d = async_get_contents("https://www.tiktok.com/oembed?url={$event->url}"); 32 | $j = json_decode($d, flags: JSON_THROW_ON_ERROR); 33 | if( 34 | !isset($j->author_unique_id) || 35 | !isset($j->title) || 36 | !isset($j->author_name) 37 | ) { 38 | $this->logger->error("bad or imcomplete response from tiktok", ['d' => $d, 'j' => $j]); 39 | return; 40 | } 41 | $reply = "[TikTok @$j->author_unique_id ($j->author_name)] $j->title"; 42 | //not sure if needed yet 43 | //$reply = html_entity_decode($reply, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 44 | $event->reply(str_replace(["\r", "\n"], " ", $reply)); 45 | } catch (\Exception $e) { 46 | echo "tiktok exception {$e->getMessage()}\n"; 47 | return; 48 | } 49 | })); 50 | } 51 | } 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /scripts/twitter/twitter.php: -------------------------------------------------------------------------------- 1 | addListener($this->handleEvents(...)); 18 | } 19 | 20 | function handleEvents(UrlEvent $event): void 21 | { 22 | if ($event->handled) 23 | return; 24 | if(!preg_match("@^https?://(?:mobile\.)?(?:twitter|x)\.com/([^/]+)/status/(\d+).*$@i", $event->url, $m)) 25 | return; 26 | $user = $m[1]; 27 | $id = $m[2]; 28 | $event->addFuture(\Amp\async(function() use ($event, $id, $user) { 29 | global $config; 30 | try { 31 | $client = HttpClientBuilder::buildDefault(); 32 | $nitters = [ 33 | "https://nitter.net/", 34 | "https://nitter.kavin.rocks/", 35 | "https://nitter.unixfox.eu/", 36 | "https://nitter.moomoo.me/", 37 | "https://nitter.mint.lgbt/", 38 | "https://nitter.esmailelbob.xyz/", 39 | "https://nitter.bird.froth.zone/", 40 | "https://nitter.privacydev.net/", 41 | "https://nitter.no-logs.com/", 42 | ]; 43 | $nitters = array_map(fn ($it) => "{$it}$user/status/$id", $nitters); 44 | $responses = []; 45 | foreach ($nitters as $nitter) { 46 | $r = new Request($nitter); 47 | $r->setTransferTimeout(5); 48 | $responses[] = \Amp\async(fn() => $client->request($r, new TimeoutCancellation(5)));; 49 | } 50 | $this->logger->info("starting requests..."); 51 | [$fails, $responses] = \Amp\Future\awaitAll($responses); 52 | $this->logger->info(count($responses) . " requests finished, " . count($fails) . " failed/timedout"); 53 | $success = []; 54 | foreach($responses as $r) { 55 | /** @var Response $r */ 56 | if($r->getStatus() == 200) { 57 | $this->logger->info("200 from {$r->getOriginalRequest()->getUri()}"); 58 | $success[] = $r; 59 | } else { 60 | $this->logger->info("failure code {$r->getStatus()} from {$r->getOriginalRequest()->getUri()}"); 61 | } 62 | } 63 | if(count($success) == 0) { 64 | $this->logger->notice("no nitters returned 200 OK"); 65 | return; 66 | } 67 | /** @var Response $response */ 68 | $response = array_pop($success); 69 | $this->logger->info("processing 200 response from " . $response->getOriginalRequest()->getUri()); 70 | $body = $response->getBody()->buffer(); 71 | 72 | $html = new HtmlDocument(); 73 | $html->load($body); 74 | $text = $html->find("div.tweet-content", 0)?->plaintext; 75 | if($text === null) { 76 | $this->logger->notice("couldnt understand response"); 77 | return; 78 | } 79 | $date = $html->find("p.tweet-published", 0)->plaintext; 80 | $date = str_replace("· ", "", $date); 81 | $date = Carbon::createFromTimeString($date, 'utc'); 82 | $ago = $date->shortRelativeToNowDiffForHumans(null, 3); 83 | $like_count = $html->find('div.icon-container span.icon-heart', 0)->parent()->plaintext; 84 | $reply_count = $html->find('div.icon-container span.icon-comment', 0)->parent()->plaintext; 85 | 86 | $event->reply("[Twitter] $ago $user tweeted: $text | {$like_count} likes, {$reply_count} replies"); 87 | } catch (\Exception $e) { 88 | echo "twitter exception {$e->getMessage()}\n"; 89 | return; 90 | } 91 | })); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/urbandict/urbandict.php: -------------------------------------------------------------------------------- 1 | ...')] 15 | function ud($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 16 | { 17 | $query = urlencode($cmdArgs['query']); 18 | try { 19 | $body = async_get_contents("http://www.urbandictionary.com/define.php?term=$query"); 20 | } catch (\async_get_exception $e) { 21 | // we get a 404 if word not found 22 | if ($e->getCode() == 404) { 23 | $bot->msg($args->chan, "ud: There are no definitions for this word."); 24 | } else { 25 | echo $e->getCode() . ' ' . $e->getMessageStripped(); 26 | $bot->msg($args->chan, "ud: Problem getting data from urbandictionary"); 27 | } 28 | return; 29 | } catch (\Exception $error) { 30 | echo $error->getMessage(); 31 | $bot->pm($args->chan, "urbandict error: {$error->getMessage()}"); 32 | return; 33 | } 34 | if (str_contains($body, "
Sorry, we couldn't find:")) { 35 | 36 | return; 37 | } 38 | $doc = new HtmlDocument($body); 39 | 40 | $defs = @$doc->find('div.definition'); 41 | 42 | // wonder if this would happen after that earlier check? 43 | if (!is_array($defs) || count($defs) < 1) { 44 | $bot->msg($args->chan, "ud: Couldn't find an entry matching {$cmdArgs['query']}"); 45 | return; 46 | } 47 | 48 | $max = 2; 49 | if ($this->server->throttle) 50 | $max = 1; 51 | $num = 0; 52 | for ($i = 0; $i < $max && isset($defs[$i]); $i++) { 53 | $def = $defs[$i]; 54 | $num++; 55 | //Haven't seen this on the pages again, maybe they stopped it 56 | if (str_contains($def->find('div.ribbon', 0)?->plaintext, "Word of the Day")) { 57 | $max++; 58 | continue; 59 | } 60 | $meaning = $def->find('div.meaning', 0)->plaintext; 61 | $example = $def->find('div.example', 0)->plaintext; 62 | $word = $def->find('a.word', 0)->plaintext; 63 | $by = $def->find('div.contributor', 0)->plaintext; 64 | 65 | $meaning = html_entity_decode($meaning, ENT_QUOTES | ENT_HTML5); 66 | $example = html_entity_decode($example, ENT_QUOTES | ENT_HTML5); 67 | $word = html_entity_decode($word, ENT_QUOTES | ENT_HTML5); 68 | $by = html_entity_decode($by, ENT_QUOTES | ENT_HTML5); 69 | 70 | $meaning = trim(str_replace(["\n", "\r"], ' ', $meaning)); 71 | 72 | $example = str_replace("\r", "\n", $example); 73 | $example = explode("\n", $example); 74 | $example = array_map('trim', $example); 75 | $example = array_filter($example); 76 | $example1line = implode(' | ', $example); 77 | 78 | $bot->msg($args->chan, "ud: $word #$num added $by"); 79 | if ($this->server->throttle) { 80 | $bot->msg($args->chan, " ├ Meaning: $meaning"); 81 | $bot->msg($args->chan, " └ $example1line"); 82 | } else { 83 | $c = 0; 84 | foreach (explode("\n", wordwrap($meaning, 80)) as $m) { 85 | $leader = $c > 0 ? "│ " : "├ "; 86 | $bot->msg($args->chan, " $leader $m"); 87 | $c++; 88 | } 89 | $c = 0; 90 | foreach ($example as $e) { 91 | $leader = $c > 0 ? "│ " : "├ "; 92 | if ($c == count($example) - 1) 93 | $leader = "└ "; 94 | $bot->msg($args->chan, " $leader $e"); 95 | $c++; 96 | } 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /scripts/user/Access.php: -------------------------------------------------------------------------------- 1 | ...')] 12 | function wiki($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 13 | { 14 | list($rpl, $rpln) = makeRepliers($args, $bot, "Wiki"); 15 | $query = rawurlencode($cmdArgs['query']); 16 | try { 17 | $body = async_get_contents("https://en.wikipedia.org/api/rest_v1/page/summary/$query"); 18 | } catch (async_get_exception $e) { 19 | if($e->getCode() == 404) { 20 | $rpl("Wikipedia does not have an article with this exact name.", "404"); 21 | return; 22 | } 23 | $rpl($e->getIRCMsg(), "error"); 24 | return; 25 | } catch (\Exception $e) { 26 | $rpl($e->getMessage(), "error"); 27 | return; 28 | } 29 | $json = json_decode($body); 30 | if($json == null) { 31 | $rpl("bad response from server", "error"); 32 | return; 33 | } 34 | $title = null; 35 | $extract = null; 36 | $url = null; 37 | if(is_string($json->title)) 38 | $title = html_entity_decode($json->title, ENT_QUOTES | ENT_HTML5); 39 | if(is_string($json->extract)) 40 | $extract = html_entity_decode($json->extract, ENT_QUOTES | ENT_HTML5); 41 | if(is_string($json->content_urls?->desktop?->page)) 42 | $url = html_entity_decode($json->content_urls->desktop->page, ENT_QUOTES | ENT_HTML5); 43 | 44 | if(strlen($extract) > 320) { 45 | $extract = explode("\n", wordwrap($extract, 320), 2)[0] . "..."; 46 | } 47 | 48 | $type = null; 49 | if($json->type != "standard") 50 | $type = $json->type; 51 | 52 | $rpl("$title - $extract - $url", $type); 53 | } -------------------------------------------------------------------------------- /scripts/wolfram/wolfram.php: -------------------------------------------------------------------------------- 1 | ...')] 11 | function calc($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 12 | { 13 | global $config; 14 | if(!isset($config['waKey'])) { 15 | echo "waKey not set in config\n"; 16 | return; 17 | } 18 | 19 | // https://products.wolframalpha.com/api/documentation?scrollTo=controlling-width-of-results 20 | // width is how many pixels rendered text would fit in 21 | $query = waURL . urlencode($cmdArgs['query']) . '&appid=' . $config['waKey'] . 22 | '&format=plaintext&location=Los+Angeles,+California&width=3000'. 23 | '&excludepodid=MakeChangeMoreThanOneCoin:QuantityData'; 24 | try { 25 | $body = async_get_contents($query); 26 | 27 | $xml = simplexml_load_string($body); 28 | $input = ''; 29 | $result = ''; 30 | $decimalAprox = ''; 31 | if ($xml['parsetimedout'] == 'true') { 32 | throw new \Exception("Error, query took too long to parse."); 33 | } 34 | if ($xml['error'] == 'true') { 35 | throw new \Exception("Error, " . @$xml->error->msg); 36 | } 37 | 38 | if ($xml['success'] == 'false') { 39 | // The input wasn't understood, show tips or didyoumeans 40 | if(isset($xml->tips)) 41 | $result = ", " . @$xml->tips->tip[0]['text']; 42 | elseif(isset($xml->didyoumeans)) 43 | $result .= ", Did you mean: " . @$xml->didyoumeans->didyoumean[0]; 44 | throw new \Exception("Query not understood" . $result); 45 | } 46 | $topPod = ''; 47 | foreach ($xml->pod as $pod) { 48 | switch($pod['id']) { 49 | case 'Input': 50 | $input = str_replace("\n", "\2;\2 ", $pod->subpod->plaintext); 51 | break; 52 | case 'Result': 53 | $result = str_replace("\n", "\2;\2 ", $pod->subpod->plaintext); 54 | break; 55 | case 'DecimalApproximation': 56 | $decimalAprox = substr($pod->subpod->plaintext, 0, 200); 57 | break; 58 | default: 59 | if($topPod == '') 60 | $topPod = str_replace("\n", "\2;\2 ", $pod->subpod->plaintext); 61 | } 62 | } 63 | if($result == "") { 64 | $result = $topPod; 65 | } 66 | if($result == "" && $decimalAprox != "") { 67 | $result = $decimalAprox; 68 | $decimalAprox = ""; 69 | } 70 | $res = "$input = $result"; 71 | if($decimalAprox != "") { 72 | $res .= " ($decimalAprox)"; 73 | } 74 | $out = explode("\n", wordwrap($res, 400, "\n", true)); 75 | $cnt = 0; 76 | foreach ($out as $msg) { 77 | $bot->pm($args->chan, "\2WA:\2 " . $msg); 78 | if($cnt++ > 4) break; 79 | } 80 | } catch (\async_get_exception $error) { 81 | echo $error->getMessage(); 82 | $bot->pm($args->chan, "\2WA:\2 {$error->getIRCMsg()}"); 83 | return; 84 | } catch (\Exception $error) { 85 | echo $error->getMessage(); 86 | $bot->pm($args->chan, "\2WA:\2 {$error->getMessage()}"); 87 | } 88 | } -------------------------------------------------------------------------------- /scripts/yoda/yoda-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knivey/lolbot/61cc4a00fd1f18c0921418537c6fc98f58d55e9e/scripts/yoda/yoda-og.png -------------------------------------------------------------------------------- /scripts/yoda/yoda.php: -------------------------------------------------------------------------------- 1 | ...')] 14 | #[Option("--og", "OG Yoda")] 15 | function yoda_cmd($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 16 | { 17 | $url = $cmdArgs['url']; 18 | 19 | if(!filter_var($url, FILTER_VALIDATE_URL)) 20 | return; 21 | 22 | try { 23 | $client = HttpClientBuilder::buildDefault(); 24 | $request = new Request($url); 25 | 26 | /** @var Response $response */ 27 | $response = $client->request($request); 28 | $body = $response->getBody()->buffer(); 29 | if ($response->getStatus() != 200) { 30 | $bot->pm($args->chan, "Server returned {$response->getStatus()}"); 31 | return; 32 | } 33 | 34 | $img = new Imagick(); 35 | try { 36 | $img->readImageBlob($body); 37 | if(!$img->getImageFormat()) { 38 | throw new \Exception(); 39 | } 40 | } catch (\Exception $e) { 41 | $bot->pm($args->chan, "couldn't recognize any image data"); 42 | return; 43 | } 44 | 45 | $yodaImg = new Imagick(); 46 | $yodaImgFile = $cmdArgs->optEnabled('--og') ? "/yoda-og.png" : "/yoda.png"; 47 | $yodaImg->readImage(__DIR__ . $yodaImgFile); 48 | $yodaImg->scaleImage($img->getImageWidth(), $img->getImageHeight()); 49 | 50 | // original behavior 51 | if ($cmdArgs->optEnabled('--og')) { 52 | $newWidth = $yodaImg->getImageWidth() + $img->getImageWidth(); 53 | $newHeight = max($yodaImg->getImageHeight(), $img->getImageHeight()); 54 | $finalImg = new Imagick(); 55 | $finalImg->newImage($newWidth, $newHeight, new ImagickPixel('transparent')); 56 | $finalImg->compositeImage($yodaImg, Imagick::COMPOSITE_OVER, 0, 0); 57 | $finalImg->compositeImage($img, Imagick::COMPOSITE_OVER, $yodaImg->getImageWidth(), 0); 58 | $img = $finalImg; 59 | } else { 60 | // new chunky behavior 61 | $img->compositeImage($yodaImg, Imagick::COMPOSITE_BLEND, 0, 0); 62 | } 63 | 64 | $tmpfile = tempnam(sys_get_temp_dir(), 'yoda') . '.webp'; 65 | $img->writeImage($tmpfile); 66 | $yodaPic = hostToFilehole($tmpfile)->await(); 67 | unlink($tmpfile); 68 | $bot->pm($args->chan, $yodaPic); 69 | } catch (\Exception $e) { 70 | return; 71 | } 72 | } 73 | 74 | #[Cmd("doubleyoda")] 75 | #[Syntax('...')] 76 | function doubleyoda_cmd($args, \Irc\Client $bot, \knivey\cmdr\Args $cmdArgs) 77 | { 78 | $url = $cmdArgs['url']; 79 | 80 | if(!filter_var($url, FILTER_VALIDATE_URL)) 81 | return; 82 | 83 | try { 84 | $body = async_get_contents($url); 85 | $img = new Imagick(); 86 | $img->readImageBlob($body); 87 | if(!$img->getImageFormat()) { 88 | throw new \Exception("data recieved not recognized as image"); 89 | } 90 | $yoda = new Imagick(__DIR__ . "/yoda-og.png"); 91 | //$yoda->scaleImage(0, $img->getImageHeight()); 92 | $final = new Imagick(); 93 | $width = $img->getImageWidth() + $yoda->getImageWidth() * 2; 94 | $height = max($img->getImageHeight(), $yoda->getImageHeight()); 95 | $final->newImage($width, $height, new ImagickPixel('transparent')); 96 | $final->compositeImage($yoda, Imagick::COMPOSITE_OVER, 0, 0); 97 | $final->compositeImage($img, Imagick::COMPOSITE_OVER, $yoda->getImageWidth(), 0); 98 | $yoda->flopImage(); 99 | $final->compositeImage($yoda, Imagick::COMPOSITE_OVER, $width - $yoda->getImageWidth(), 0); 100 | $tmpfile = tempnam(sys_get_temp_dir(), 'yoda') . '.webp'; 101 | $final->writeImage($tmpfile); 102 | $yodaPic = hostToFilehole($tmpfile)->await(); 103 | unlink($tmpfile); 104 | $bot->pm($args->chan, $yodaPic); 105 | } catch (\Exception $e) { 106 | $bot->pm($args->chan, "yoda troubles: {$e->getMessage()}"); 107 | return; 108 | } 109 | } 110 | 111 | /** 112 | * 113 | * @param string $filename 114 | * @return Future 115 | */ 116 | function hostToFilehole(string $filename): Future 117 | { 118 | return \Amp\async(function () use ($filename) { 119 | if(!file_exists($filename)) 120 | throw new \Exception("hostToFilehole called with non existant filename: $filename"); 121 | $client = HttpClientBuilder::buildDefault(); 122 | $request = new Request("https://upload.beer", "POST"); 123 | $body = new Form(); 124 | $body->addField('url_len', '5'); 125 | $body->addField('expiry', '86400'); 126 | $body->addFile('file', $filename); 127 | $request->setBody($body); 128 | //var_dump($request); 129 | /** @var Response $response */ 130 | $response = $client->request($request); 131 | //var_dump($response); 132 | if ($response->getStatus() != 200) { 133 | throw new \Exception("upload.beer returned {$response->getStatus()}"); 134 | } 135 | $respBody = $response->getBody()->buffer(); 136 | return $respBody; 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /scripts/yoda/yoda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knivey/lolbot/61cc4a00fd1f18c0921418537c6fc98f58d55e9e/scripts/yoda/yoda.png -------------------------------------------------------------------------------- /scripts/zyzz/zyzz.php: -------------------------------------------------------------------------------- 1 | pm($args->chan, "empty quotes file"); 14 | return; 15 | } 16 | $fact = $facts[array_rand($facts)]; 17 | 18 | $bot->pm($args->chan, $fact); 19 | } -------------------------------------------------------------------------------- /scripts/zyzz/zyzz.txt: -------------------------------------------------------------------------------- 1 | Personally I’m a very friendly and happy person, and believe that people need to lighten up and enjoy life more. Even though I put on a lot of size and to some look intimidating, I’m one of the friendliest people you can meet. I love playing up my perceived stereotype, and at the end of the day, never take myself seriously, which is one of the reasons I have accrued the fan base that I have. 2 | My message is to train hard, don't be a hard cunt, enjoy life and don't take yourself too seriously. If there were more people like me, the world will be a happier place. Id rather do what i do and have fun then sit back hating on someone achieving something. 3 | I don't workout for chicks, i workout to create an aura, when i walk into a room, introduce myself to someone, go for a job interview, i love looking dominant, in charge, and in control. I love walking past and having people point and talk about me, i love the fact that when i go somewhere with thousands of people, almost all of them will remember who i am when it is over, and i didn't even have to say a word. 4 | There are normal people. There are try hards. There are hard cunts. Then there are sick cunts. Sick cunts are the best, friendly, sexy, aesthetic, party hard and live it up without being staunch and get along with everyone. 5 | Everybody, one day, will die and be forgotten. Act and behave in a way that will make life interesting and fun, fuck a mundane, predictable life working Monday to Friday with something you derive no pleasure from; just living life out till you grow old and wither away. Find a passion, form relationships, don't be afraid to get out there and fuck what everyone else thinks, trust me it's a lot more fun that way.. 6 | At the end off the day bro you got to listen to this, if your a fucking shredded sick kunt you can get away with anything bro. If your some fat kunt making this shit up people will be like aww yeahh zyzz bro yeah, they wont give a fuck man. 7 | I want to move my life in other directions and have found myself far too involved in a lifestyle with no genuine substance, meeting far too many fake people/girls who if i didn't look the way i did, would have never given me the time of day.. Time to get back to studying, there's more to life than partying. 8 | There is no 'Zyzz'.... Everyone has a little bit of Zyzz in them. 9 | Don’t ever pay people out or put people down. Instead just put yourself up and let the haters do their thing. Id rather be a person that’s hated on, than a person that does the hating. A wise man one said.. Haters gonna hate! 10 | Stop giving a fuck what girls think and say whatever the fuck you want to say and do whatever you want to do. 11 | None of this will make sense to any of you once you get the physique and know whats its like, i know it sounds like im bragging, but think of it as driving a ferrari in real life. why do people drive ferrarris? to get attention, looks, extert dominance and superiority from the norm, and to get people talking… but it also inspires jealousy and haters on the person driving. thats exactly what i experience irl. 12 | I always feel i have to train and diet my hardest now because i have a look/image to live up to. People talk and already have high expectations from what they hear about me, so i would never let myself look sloppy. If anything, all this has made me want to train harder, and diet more intensely. Fuk ‘good enough',, perfection is my goal. 13 | Fuck that shit brah, you can either be a sick cunt or you can be a sad cunt, don't be a sad cunt brah 14 | Nah brah I'm sweet. 15 | FUAAAAAAAAAAAARK 16 | We're all gonna make it brah 17 | Fuckin ripped, yeah cunt! 18 | It's fucking ZEEEEZ man! And I'm coming at you bro 19 | You mirin'? 20 | Fucking coked up brah 21 | 22 | --------------------------------------------------------------------------------