diff --git a/app/DiscordWebhookMessage.php b/app/DiscordWebhookMessage.php new file mode 100644 index 0000000..01d6d1c --- /dev/null +++ b/app/DiscordWebhookMessage.php @@ -0,0 +1,36 @@ +morphToMany("App\TwitchWebhook", "twitch_webhook_events"); + } +} diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php index 497cc9d..db0f205 100644 --- a/app/Http/Controllers/WebhookController.php +++ b/app/Http/Controllers/WebhookController.php @@ -2,24 +2,24 @@ namespace App\Http\Controllers; +use App\DiscordWebhookMessage; use App\TwitchWebhook; use App\WebhookAction; use App\Http\Requests\AddWebhookAction; +use App\Http\Requests\CreateDiscordWebhookMessage; use App\Http\Requests\SubscribeWebhook; use App\Http\Requests\TestWebhookAction; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Storage; use romanzipp\Twitch\Facades\Twitch; use Woeler\DiscordPhp\Webhook\DiscordWebhook; use Woeler\DiscordPhp\Message\DiscordEmbedMessage; use Woeler\DiscordPhp\Message\DiscordTextMessage; -use Illuminate\Support\Facades\Storage; - // TODO: document all the methods/constants -class WebhookController extends Controller -{ +class WebhookController extends Controller { // 10 days const MAX_LEASE_SECONDS = 864000; const TOPIC_STREAM_CHANGED = "https://api.twitch.tv/helix/streams"; @@ -57,26 +57,41 @@ class WebhookController extends Controller } // TODO: implement in a future more than one action per hook - public function addAction(AddWebhookAction $request) { + public function addAction(CreateDiscordWebhookMessage $request) { $hook = Auth::user()->twitchWebhooks()->where("type", $request->type)->firstOrFail(); - // TODO: currently hardcoded, allow only one action - $actions = $hook->webhookActions(); + // currently hardcoded, allow only one action + $discordWebhookMessage = $hook->discordWebhookMessage(); // add - if (count($actions->get()) == 0) { - $action = $actions->create([ - "discord_hook_url" => $request->discord_hook_url - ]); + if (count($discordWebhookMessage->get()) == 0) { + $action = $discordWebhookMessage->create($request->all()); // edit the existing one } else { - $action = $actions->first(); - $action->discord_hook_url = $request->discord_hook_url; + $action = $discordWebhookMessage->first(); + $action->fill($request->all()); $action->save(); } - return response()->json(["action_id" => $action->id]); + if ($request->hasFile("avatar")) { + if (!$request->file("avatar")->isValid()) { + // TODO display error message + abort(400); + return; + } + + $avatarPath = $request->avatar->storeAs("discordWebhookMessages", Auth::user()->twitch_uid . "." . $request->avatar->extension(), "public"); + $action->avatar = $avatarPath; + $action->save(); + + } else if ($request->delete_avatar && $action->avatar) { + Storage::disk("public")->delete($action->avatar); + $action->avatar = null; + $action->save(); + } + + return response()->json(["action_id" => $action->id, "avatar" => $action->avatar ? Storage::disk("public")->url($action->avatar) : '']); } /** @@ -84,11 +99,11 @@ class WebhookController extends Controller */ public function testWebhook(TestWebhookAction $request) { $message = new DiscordTextMessage(); - $message->setContent("Test message.") - ->setUsername(config("app.name")) - ->setAvatar(asset("img/twitch-icon.png")); + $message->setContent($request->content ?? "Test message.") + ->setUsername($request->username ?? config("app.name")) + ->setAvatar($request->avatar ?? asset("img/twitch-icon.png")); - $webhook = new DiscordWebhook($request->discord_hook_url); + $webhook = new DiscordWebhook($request->discord_webhook_url); $webhook->send($message); return response()->json($webhook); @@ -165,25 +180,13 @@ class WebhookController extends Controller } /** - * Stream change hook gets here, executes the user configured hook actions + * Know the stream update type sent by Twitch, returns "start", "update", or "end" */ - // TODO: test this, separate the 3 cases: start, update, end - public function streamUpdate(Request $request) { - $twitchWebhook = TwitchWebhook::findOrFail($request->hook_id); - - if (!$this->verifySecret()) { - abort(403); - - } else if ($twitchWebhook->disabled) { - return response("Webhook disabled", 200); - } - - $eventType = null; + private function streamUpdateType(TwitchWebhook $twitchWebhook, Request $request) { $data = $request->all()["data"]; Storage::put("stream_hook.json", json_encode($data)); if ($data) { - $data = $data[0]; if (empty($twitchWebhook->live_since)) { @@ -203,10 +206,30 @@ class WebhookController extends Controller $twitchWebhook->save(); } + return $eventType; + } + + /** + * Stream change hook gets here, executes the user configured hook actions + */ + public function streamUpdate(Request $request) { + $twitchWebhook = TwitchWebhook::findOrFail($request->hook_id); + + if (!$this->verifySecret()) { + abort(403); + + } else if ($twitchWebhook->disabled) { + return response("Webhook disabled", 200); + } + + $eventType = $this->streamUpdateType($twitchWebhook, $request); + if ( - ($eventType === "start" && $twitchWebhook->webhookActions) || - (config("app.env") !== "production" && $twitchWebhook->webhookActions && $eventType !== "end") - ) { + ($eventType === "start" && $twitchWebhook->discordWebhookMessage) || + (config("app.env") !== "production" && $twitchWebhook->discordWebhookMessage && $eventType !== "end") + ) { + $data = $request->all()["data"][0]; + // TODO: move this (generation of the message embed) to a separate method $userInfo = Twitch::getUserById(intval($data["user_id"]))->data[0]; $gameInfo = null; @@ -215,24 +238,8 @@ class WebhookController extends Controller $gameInfo = Twitch::getGameById(intval($data["game_id"]))->data[0]; } - $embed = new DiscordEmbedMessage(); - $embed->setUsername(config("app.name")) - ->setAvatar(asset("img/twitch-icon.png")) - ->setContent("¡Hola @everyone! " . $data["user_name"] . " está ahora en directo https://twitch.tv/" . $userInfo->login . " ! Ven a verle :wink:!") - // embed - ->setColor(hexdec(substr("#9147ff", 1))) - ->setAuthorName($userInfo->display_name) - ->setAuthorIcon($userInfo->profile_image_url) - ->setThumbnail($userInfo->profile_image_url) - ->setImage(str_replace(["{width}", "{height}"], [1280, 720], $data["thumbnail_url"]) . "?cache=" . Carbon::now()->timestamp) - ->setTitle($data["title"]) - ->setUrl("https://twitch.tv/$userInfo->login") - ->addField("Juego", $gameInfo ? $gameInfo->name : "─", true) - ->addField("Espectadores", $data["viewer_count"], true); - - foreach($twitchWebhook->webhookActions as $hookAction) { - $webhook = new DiscordWebhook($hookAction->discord_hook_url); - $webhook->send($embed); + foreach($twitchWebhook->discordWebhookMessage as $msg) { + $this->sendDiscordWebhookMessage($msg, $data, $userInfo, $gameInfo); } } @@ -240,6 +247,52 @@ class WebhookController extends Controller return response("Ok", 200); } + /** + * Send a message to using the DiscordWebhookMessage preferences and the given data + */ + private function sendDiscordWebhookMessage(DiscordWebhookMessage $msg, $data, $userInfo, $gameInfo) { + $hasEmbed = $msg->has_embed; + if ($hasEmbed) { + $discordMessage = new DiscordEmbedMessage(); + } else { + $discordMessage = new DiscordTextMessage(); + } + + $discordMessage->setUsername($msg->username ?? config("app.name")) + ->setAvatar($msg->avatar ? Storage::disk("public")->url($msg->avatar) . "?cache=" . Carbon::now()->timestamp : asset("img/twitch-icon.png")) + ->setContent($msg->content ?? "¡Hola @everyone! " . $userInfo->display_name . " está ahora en directo https://twitch.tv/" . $userInfo->login . " ! Ven a verle :wink:!"); + + if ($hasEmbed) { + $discordMessage->setColor(hexdec(substr($msg->embed_color ?? "#9147ff", 1))) + ->setAuthorName($msg->embed_author_name ? $userInfo->display_name : "") + ->setAuthorIcon($msg->embed_author_icon ? $userInfo->profile_image_url : "") + ->setAuthorUrl($msg->embed_author_url ? "https://twitch.tv/$userInfo->login" : "") + ->setThumbnail($msg->embed_thumbnail ? $userInfo->profile_image_url : "") + ->setImage($msg->embed_image ? str_replace(["{width}", "{height}"], [1280, 720], $data["thumbnail_url"]) . "?cache=" . Carbon::now()->timestamp : "") + ->setTitle($msg->embed_title ?? $data["title"]) + ->setDescription($msg->embed_description ?? "") + ->setUrl($msg->embed_url ?? "https://twitch.tv/$userInfo->login"); + + foreach(json_decode($msg->embed_fields) as $field) { + $value; + switch($field->value) { + case "%game%": + $value = $gameInfo ? $gameInfo->name : "─"; + break; + case "%viewer_count%": + $value = $data["viewer_count"]; + break; + default: + $value = $field->value; + } + $discordMessage->addField($field->name, $value, $field->inline); + } + } + + $webhook = new DiscordWebhook($msg->discord_webhook_url); + $webhook->send($discordMessage); + } + // TODO: implement the method public function subscription(Request $request) { if (!$this->verifySecret()) { diff --git a/app/Http/Requests/CreateDiscordWebhookMessage.php b/app/Http/Requests/CreateDiscordWebhookMessage.php new file mode 100644 index 0000000..25f0dfc --- /dev/null +++ b/app/Http/Requests/CreateDiscordWebhookMessage.php @@ -0,0 +1,50 @@ + "required|boolean", + "discord_webhook_url" => "required|url|starts_with:https://discordapp.com/api/webhooks/,https://discord.com/api/webhooks/", + "username" => "nullable|string|max:50", + "avatar" => "nullable|file|image", + "delete_avatar" => "nullable|in:true,1", + "content" => "nullable|string|max:2000", + "embed_color" => [ + "nullable", + "string", + "regex:/^#[0-9A-F]{6}$/i" + ], + "embed_author_name" => "nullable|boolean", + "embed_author_url" => "nullable|boolean", + "embed_author_icon" => "nullable|boolean", + "embed_thumbnail" => "nullable|boolean", + "embed_title" => "nullable|string|max:191", + "embed_description" => "nullable|string|max:2000", + "embed_url" => "nullable|url|max:191", + "embed_image" => "nullable|boolean", + "embed_fields" => "nullable|json" + ]; + } +} diff --git a/app/Http/Requests/TestWebhookAction.php b/app/Http/Requests/TestWebhookAction.php index daf6bde..396bcec 100644 --- a/app/Http/Requests/TestWebhookAction.php +++ b/app/Http/Requests/TestWebhookAction.php @@ -25,7 +25,10 @@ class TestWebhookAction extends FormRequest public function rules() { return [ - "discord_hook_url" => "required|url|starts_with:https://discordapp.com/api/webhooks/,https://discord.com/api/webhooks/" + "discord_webhook_url" => "required|url|starts_with:https://discordapp.com/api/webhooks/,https://discord.com/api/webhooks/", + "username" => "nullable|string|max:50", + "avatar" => "nullable|url", + "content" => "nullable|string|max:2000" ]; } } diff --git a/app/TwitchWebhook.php b/app/TwitchWebhook.php index 4de2f16..0c42b0a 100644 --- a/app/TwitchWebhook.php +++ b/app/TwitchWebhook.php @@ -34,4 +34,13 @@ class TwitchWebhook extends Model public function webhookActions() { return $this->hasMany("App\WebhookAction", "webhook_id"); } + + // event handlers + + /** + * Get discord message/embed assigned to this twitch webhook + */ + public function discordWebhookMessage() { + return $this->morphedByMany("App\DiscordWebhookMessage", "twitch_webhook_event"); + } } diff --git a/database/migrations/2020_04_30_155044_create_twitch_webhook_events_table.php b/database/migrations/2020_04_30_155044_create_twitch_webhook_events_table.php new file mode 100644 index 0000000..5cf0f3f --- /dev/null +++ b/database/migrations/2020_04_30_155044_create_twitch_webhook_events_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId("twitch_webhook_id")->references("id")->on("twitch_webhooks")->onUpdate("cascade")->onDelete("cascade"); + $table->unsignedBigInteger("twitch_webhook_event_id"); + $table->string("twitch_webhook_event_type"); + $table->unique(["twitch_webhook_id", "twitch_webhook_event_type"]); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('twitch_webhook_events'); + } +} diff --git a/database/migrations/2020_04_30_163539_create_discord_webhook_messages_table.php b/database/migrations/2020_04_30_163539_create_discord_webhook_messages_table.php new file mode 100644 index 0000000..6980300 --- /dev/null +++ b/database/migrations/2020_04_30_163539_create_discord_webhook_messages_table.php @@ -0,0 +1,47 @@ +id(); + $table->boolean("enabled")->default(true); + $table->string("discord_webhook_url"); + $table->string("username")->nullable(); + $table->string("avatar")->nullable(); + $table->text("content")->nullable(); + $table->boolean("has_embed")->default(false); + $table->string("embed_color", 7)->default("#9147ff"); + $table->boolean("embed_author_name")->default(false); + $table->boolean("embed_author_url")->default(false); + $table->boolean("embed_author_icon")->default(false); + $table->boolean("embed_thumbnail")->default(false); + $table->string("embed_title")->nullable(); + $table->text("embed_description")->nullable(); + $table->string("embed_url")->nullable(); + $table->boolean("embed_image")->default(false); + $table->json("embed_fields")->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('discord_webhook_messages'); + } +} diff --git a/resources/js/components/auth/StreamEventComponent.vue b/resources/js/components/auth/StreamEventComponent.vue new file mode 100644 index 0000000..9766007 --- /dev/null +++ b/resources/js/components/auth/StreamEventComponent.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/resources/js/components/auth/hookActions/DiscordWebhookMessageComponent.vue b/resources/js/components/auth/hookActions/DiscordWebhookMessageComponent.vue new file mode 100644 index 0000000..49ff348 --- /dev/null +++ b/resources/js/components/auth/hookActions/DiscordWebhookMessageComponent.vue @@ -0,0 +1,420 @@ + + + \ No newline at end of file diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 5462090..eea9f1e 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -13,5 +13,4 @@ @endif

Site under construction

- @endsection diff --git a/resources/views/me/dashboard.blade.php b/resources/views/me/dashboard.blade.php index c128a08..6640006 100644 --- a/resources/views/me/dashboard.blade.php +++ b/resources/views/me/dashboard.blade.php @@ -9,197 +9,14 @@

Available events

-
-

- -

-
-

- A message will be sent to the channel when a stream is started. -

-
- disabled || $streamHook->disabled_at)) ? "disabled" : "" }} /> - - -
-
-
+ - /* mask the hook url unless editing or empty */ - #discord-webhook input[type="url"]:not(:focus):not(:placeholder-shown) { - filter: blur(4px); - } - - - + discord-webhook-message="{{ ($streamHook && $streamHook->discordWebhookMessage()->first()) ? $streamHook->discordWebhookMessage()->first() : "{}" }}" + >