323 lines
11 KiB
PHP
323 lines
11 KiB
PHP
<?php
|
|
|
|
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;
|
|
|
|
// TODO: document all the methods/constants
|
|
class WebhookController extends Controller {
|
|
// 10 days
|
|
const MAX_LEASE_SECONDS = 864000;
|
|
const TOPIC_STREAM_CHANGED = "https://api.twitch.tv/helix/streams";
|
|
|
|
public function __construct() {
|
|
$this->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(CreateDiscordWebhookMessage $request) {
|
|
$hook = Auth::user()->twitchWebhooks()->where("type", $request->type)->firstOrFail();
|
|
|
|
// currently hardcoded, allow only one action
|
|
$discordWebhookMessage = $hook->discordWebhookMessage();
|
|
|
|
// add
|
|
if (count($discordWebhookMessage->get()) == 0) {
|
|
$action = $discordWebhookMessage->create($request->all());
|
|
|
|
// edit the existing one
|
|
} else {
|
|
$action = $discordWebhookMessage->first();
|
|
$action->fill($request->all());
|
|
$action->save();
|
|
}
|
|
|
|
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) : '']);
|
|
}
|
|
|
|
/**
|
|
* Sends a message to a given discord webhook url
|
|
*/
|
|
public function testWebhook(TestWebhookAction $request) {
|
|
$message = new DiscordTextMessage();
|
|
$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_webhook_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";
|
|
}
|
|
|
|
/**
|
|
* Know the stream update type sent by Twitch, returns "start", "update", or "end"
|
|
*/
|
|
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)) {
|
|
$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();
|
|
}
|
|
|
|
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->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;
|
|
|
|
if ($data["game_id"]) {
|
|
$gameInfo = Twitch::getGameById(intval($data["game_id"]))->data[0];
|
|
}
|
|
|
|
foreach($twitchWebhook->discordWebhookMessage as $msg) {
|
|
$this->sendDiscordWebhookMessage($msg, $data, $userInfo, $gameInfo);
|
|
}
|
|
|
|
}
|
|
|
|
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()) {
|
|
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;
|
|
}
|
|
}
|