middleware("auth")->only([ "subscribe", "addAction" ]); } /** * Subscribe or unsubscribe from a hook event */ public function subscribe(SubscribeWebhook $request) { if ($request->mode === "subscribe") { switch($request->type) { case "stream": return $this->subscribeStreamHook($request); break; } } else if ($request->mode === "unsubscribe") { $hook = Auth::user()->twitchWebhooks()->where("type", $request->type)->first(); if ($hook) { $hook->disabled_at = Carbon::now(); $hook->save(); } return response("Ok"); } } // TODO: implement in a future more than one action per hook public function addAction(AddWebhookAction $request) { $hook = Auth::user()->twitchWebhooks()->where("type", $request->type)->firstOrFail(); // TODO: currently hardcoded, allow only one action $actions = $hook->webhookActions(); // add if (count($actions->get()) == 0) { $action = $actions->create([ "discord_hook_url" => $request->discord_hook_url ]); // edit the existing one } else { $action = $actions->first(); $action->discord_hook_url = $request->discord_hook_url; $action->save(); } return response()->json(["action_id" => $action->id]); } /** * Sends a message to a given discord webhook url */ public function testWebhook(TestWebhookAction $request) { $message = new DiscordTextMessage(); $message->setContent("Test message.") ->setUsername(config("app.name")) ->setAvatar(asset("img/twitch-icon.png")); $webhook = new DiscordWebhook($request->discord_hook_url); $webhook->send($message); return response()->json($webhook); } /** * Create a webhook to listen for stream changes */ private function subscribeStreamHook(Request $request) { $hookCallback = route("hooks.twitch.stream"); $lease = self::MAX_LEASE_SECONDS; if (config("app.env") !== "production") { $lease = 3600; } $topic = self::TOPIC_STREAM_CHANGED . "?user_id=" . Auth::user()->twitch_uid; // TODO: implement secrets, global or specified for each hoock? // $secret = "create a random secret"; $twitchWebhook = Auth::user()->twitchWebhooks()->firstOrNew([ "type" => $request->type ], [ "topic" => $topic, "callback" => $hookCallback, "expires_at" => Carbon::now()->addSeconds($lease) ]); $twitchWebhook->disabled = false; $twitchWebhook->disabled_at = null; if (!$twitchWebhook->id) { $twitchWebhook->save(); $twitchWebhook->callback .= "?hook_id=" . $twitchWebhook->id; $subscription = Twitch::subscribeWebhook($twitchWebhook->callback, $topic, $lease); } else if (config("app.env") !== "production") { $subscription = Twitch::subscribeWebhook($twitchWebhook->callback, $topic, $lease); $twitchWebhook->expires_at = Carbon::now()->addSeconds($lease); } $twitchWebhook->save(); if (isset($subscription) && $subscription->success) { return response("Ok"); } else { $twitchWebhook->disabled = true; $twitchWebhook->save(); return response()->json($subscription, $subscription->status); } } /** * Verifies the webhook (reply to Twitch GET callback after hook creation) */ public function verifySubscription(Request $request) { $twitchHook = TwitchWebhook::findOrFail($request->hook_id); $twitchHook->active = true; $twitchHook->save(); return response($request->input("hub_challenge"), 200)->header("Content-Type", "text/plain"); } // TODO: implement the method // listen to follow events public function follow(Request $request) { if (!$this->verifySecret()) { abort(403); } return "follow"; } /** * Stream change hook gets here, executes the user configured hook actions */ // 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; $data = $request->all()["data"]; Storage::put("stream_hook.json", json_encode($data)); if ($data) { $data = $data[0]; if (empty($twitchWebhook->live_since)) { $eventType = "start"; $twitchWebhook->live_since = Carbon::now(); $twitchWebhook->offline_since = null; $twitchWebhook->save(); } else { $eventType = "update"; } } else { $eventType = "end"; $twitchWebhook->live_since = null; $twitchWebhook->offline_since = Carbon::now(); $twitchWebhook->save(); } if ( ($eventType === "start" && $twitchWebhook->webhookActions) || (config("app.env") !== "production" && $twitchWebhook->webhookActions && $eventType !== "end") ) { // TODO: move this (generation of the message embed) to a separate method $userInfo = Twitch::getUserById(intval($data["user_id"]))->data[0]; $gameInfo = null; if ($data["game_id"]) { $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); } } return response("Ok", 200); } // TODO: implement the method public function subscription(Request $request) { if (!$this->verifySecret()) { abort(403); } return "subscription"; } // TODO: verify that the method works ok private function verifySecret(String $secret = null) { if ($secret) { $X_HUB_SIGNATURE = null; if (isset($_SERVER["HTTP_X_HUB_SIGNATURE"])) { $X_HUB_SIGNATURE = $_SERVER["HTTP_X_HUB_SIGNATURE"]; } $data = file_get_contents("php://input"); $hmac = hash_hmac("sha256", $data, $secret); return $hmac == $X_HUB_SIGNATURE; } return true; } }