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 @@
+
+
+
+
+
- A message will be sent to the channel when a stream is started. -
-