1
0

First commit

This commit is contained in:
2020-04-29 23:58:18 +02:00
commit f3230a32ce
108 changed files with 77210 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers;
use App\User;
use App\UserAccessToken;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use romanzipp\Twitch\Facades\Twitch;
use romanzipp\Twitch\Enums\Scope;
class UserController extends Controller
{
const OAUTH_BASE_URI = "https://id.twitch.tv/oauth2/";
const AUTH_REDIRECT_ROUTE = "me.dashboard";
public function __construct() {
$this->middleware("guest")->only(["login", "authorized"]);
$this->middleware("auth")->except(["login", "authorized"]);
}
/**
* Redirect to the Twitch OAuth app authorization site.
*/
public function login() {
$stateToken = getToken(25);
session()->flash("stateToken", $stateToken);
return redirect()->away(Twitch::getOAuthAuthorizeUrl("code", [
Scope::USER_READ_EMAIL
], $stateToken));
}
/**
* Callback of the Twitch OAuth authorization page.
*
* @param Illuminate\Http\Request $request
*/
public function authorized(Request $request) {
if ($request->error) {
session()->flash("error", [
"title" => "Authorization error",
"description" => $request->error_description . "."
]);
return redirect(route("home"));
} else if (session("stateToken") && session("stateToken") != $request->state) {
session()->flash("error", [
"title" => "Missmatch state token",
"description" => "State tokens do not match, maybe someone tried to intercept the login attempt. Try again in sone seconds, if it persists contact with the site administrator."
]);
return redirect(route("home"));
}
$oauthTokenRequest = Twitch::getOAuthToken($request->code);
if (!$oauthTokenRequest->success) {
return response("Error " . $oauthTokenRequest->data->status . ", " . $oauthTokenRequest->data->message);
}
$userAccessToken = $oauthTokenRequest->data->access_token;
$user = $this->getAuthedUser($userAccessToken);
$dbUser = User::where("twitch_uid", $user->id)->first();
if (!$dbUser) {
$dbUser = $this->store($user);
}
$dbUser->accessTokens()->create([
"access_token" => $userAccessToken,
"refresh_token" => $oauthTokenRequest->data->refresh_token,
"expires_in" => $oauthTokenRequest->data->expires_in
]);
Auth::login($dbUser);
session(["oauthToken" => $userAccessToken]);
return redirect(route(self::AUTH_REDIRECT_ROUTE));
}
/**
* Get data of a user given a user access token
*
* @param string $userAccessToken A valid user access token obtained from a OAuth token request.
*/
private function getAuthedUser($userAccessToken) {
$userRequest = Twitch::withToken($userAccessToken)->getAuthedUser();
if (!$userRequest->success) {
session()->flash("error", [
"title" => "Could not get user data",
"description" => "Something failed while trying to get the user data, is Twitch API down?"
]);
return redirect(route("home"));
}
return $userRequest->data[0];
}
/**
* Creates a user record on the database given his data
*
* @param Array $data Array that contains the data of the user (id, display_name, email and profile_image_url).
*/
private function store($data) {
$user = new User;
$user->twitch_uid = $data->id;
$user->username = $data->display_name;
$user->email = $data->email;
$user->profile_image = $data->profile_image_url;
$user->save();
return $user;
}
/**
* Logout the user from the session and destroy the user access token created for the session.
*
* @param Illuminate\Http\Request $request
*/
public function logout(Request $request) {
$logoutRequest = $this->invalidateUserOauthToken(session("oauthToken"));
if (!$logoutRequest->success) {
session()->flash("error", [
"title" => "Session could not be ended",
"description" => "The user oauth token could not be invalidated, is Twitch API down?"
]);
return redirect(route("home"));
}
Auth::user()->accessTokens()->where("access_token", session("oauthToken"))->first()->delete();
Auth::logout();
session()->forget("oauthToken");
return redirect(route("home"));
}
/**
* Invalidates the given user oauth token.
*
* @param string $oauthToken user oauth token to be invalidated.
*/
private function invalidateUserOauthToken($oauthToken) {
return Twitch::post(self::OAUTH_BASE_URI . "revoke", [
"client_id" => Twitch::getClientId(),
"token" => $oauthToken
]);
}
/**
* Display the user dashboard.
*/
public function showDashboard() {
return view("me.dashboard", [
"followHook" => Auth::user()->twitchWebhooks()->where("type", "follow")->first(),
"streamHook" => Auth::user()->twitchWebhooks()->where("type", "stream")->first(),
"subscriptionHook" => Auth::user()->twitchWebhooks()->where("type", "subscription")->first(),
]);
}
}

View File

@@ -0,0 +1,258 @@
<?php
namespace App\Http\Controllers;
use App\TwitchWebhook;
use App\WebhookAction;
use App\Http\Requests\AddWebhookAction;
use App\Http\Requests\SubscribeWebhook;
use App\Http\Requests\TestWebhookAction;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
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
{
// 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(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(config("app.url") . "/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;
$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);
}
$twitchWebhook->save();
if ((isset($subscription) && $subscription->success) || true) {
return response("Ok");
} else {
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 ($twitchWebhook->webhookActions) {
// 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("https://tse1.mm.bing.net/th?id=OIP.kncd96A4OjouPDpT8ymkIAHaHa&pid=Api&f=1")
->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) {
// TODO: change the url with the appropiate one
$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;
}
}