Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/glimesh/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ defmodule Glimesh.Api do
chat: false,
streamkey: false,
follow: false,
stream_info: false
stream_info: false,
interactive: false
}
end

Expand Down
48 changes: 48 additions & 0 deletions lib/glimesh/api/resolvers/channel_resolver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Glimesh.Api.ChannelResolver do
@moduledoc false
import Ecto.Query

alias Absinthe.Subscription
alias Glimesh.Api
alias Glimesh.ChannelCategories
alias Glimesh.ChannelLookups
Expand Down Expand Up @@ -275,6 +276,53 @@ defmodule Glimesh.Api.ChannelResolver do
|> Api.connection_from_query_with_count(args)
end

# Sends a message over client id
def send_interactive_message(_parent, args, %{context: %{access: %{access_type: "app"}}}) do
event_name = Map.get(args, :event_name)
session = Map.get(args, :session_id)
data = Map.get(args, :data)

Subscription.publish(
GlimeshWeb.Endpoint,
%{data: data, event_name: event_name, authorized: false},
Keyword.put([], :interactive, "streams:interactive:#{session}")
)

{:ok, %{data: data, event_name: event_name, authorized: false}}
end

# Sends a message with an access token
def send_interactive_message(_parent, args, %{context: %{access: access}}) do
# Get the data from the message
event_name = Map.get(args, :event_name)
session = Map.get(args, :session_id)
data = Map.get(args, :data)

# Check that it has interactive scope, channel matches sessionID
# If so it is an authorized message, if not it isn't
with true <- Map.get(access.scopes, :interactive),
%Glimesh.Streams.Channel{} = channel <-
ChannelLookups.get_channel_for_username(access.user.username),
true <- channel.id == session do
Subscription.publish(
GlimeshWeb.Endpoint,
%{data: data, event_name: event_name, authorized: true},
Keyword.put([], :interactive, "streams:interactive:#{session}")
)

{:ok, %{data: data, event_name: event_name, authorized: true}}
else
_ ->
Subscription.publish(
GlimeshWeb.Endpoint,
%{data: data, event_name: event_name, authorized: false},
Keyword.put([], :interactive, "streams:interactive:#{session}")
)

{:ok, %{data: data, event_name: event_name, authorized: false}}
end
end

def get_moderation_logs(args, %{source: channel}) do
Streams.ChannelModerationLog
|> where(channel_id: ^channel.id)
Expand Down
29 changes: 29 additions & 0 deletions lib/glimesh/api/schema/channel_types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Glimesh.Api.ChannelTypes do

import Absinthe.Resolution.Helpers
import_types(Absinthe.Plug.Types)
import_types(Glimesh.Api.Schema.Types.Interactive)

alias Glimesh.Api
alias Glimesh.Api.ChannelResolver
Expand Down Expand Up @@ -107,6 +108,15 @@ defmodule Glimesh.Api.ChannelTypes do
resolve(&ChannelResolver.upload_stream_thumbnail/3)
end

@desc "Send a message over the interactive session"
field :send_interactive_message, type: :interactive do
arg(:session_id, non_null(:integer))
arg(:event_name, non_null(:string))
arg(:data, non_null(:json), description: "JSON data to send")

resolve(&ChannelResolver.send_interactive_message/3)
end

@desc "Update a channel's stream info"
field :update_stream_info, type: :channel do
arg(:channel_id, non_null(:id))
Expand All @@ -127,6 +137,16 @@ defmodule Glimesh.Api.ChannelTypes do
end
end)
end

field :interactive, :interactive do
arg(:session, non_null(:integer), description: "The interactive session ID")

config(fn args, _ ->
case Map.get(args, :session) do
session -> {:ok, topic: [Streams.get_subscribe_topic(:interactive, session)]}
end
end)
end
end

@desc "Current channel status"
Expand All @@ -135,6 +155,15 @@ defmodule Glimesh.Api.ChannelTypes do
value(:offline, as: "offline")
end

@desc "An interactive packet"
object :interactive do
field :event_name, :string, description: "The name of the event"
field :data, :json, description: "The data sent across the connection"
field :authorized, :boolean, description: "Was this an authorized message?"
field :inserted_at, non_null(:naive_datetime), description: "Interactive creation date"
field :updated_at, non_null(:naive_datetime), description: "Interactive updated date"
end

@desc "Categories are the containers for live streaming content."
object :category do
field :id, :id, description: "Unique category identifier"
Expand Down
40 changes: 40 additions & 0 deletions lib/glimesh/api/schema/interactive_types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Glimesh.Api.Schema.Types.Interactive do
@moduledoc """
The Json scalar type allows arbitrary JSON values to be passed in and out.
Requires `{ :jason, "~> 1.1" }` package: https://github.com/michalmuskala/jason
We use this to make sure the user is sending valid json in an interactive message
"""
use Absinthe.Schema.Notation

scalar :json, name: "JSON" do
description("""
JSON data surrounded by a string.
"{
\"test\": 123
}"
""")

serialize(&encode/1)
parse(&decode/1)
end

# Decods the json, returns it if valid. Fails if not
@spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error
@spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
defp decode(%Absinthe.Blueprint.Input.String{value: value}) do
case Jason.decode(value) do
{:ok, result} -> {:ok, result}
_ -> :error
end
end

defp decode(%Absinthe.Blueprint.Input.Null{}) do
{:ok, nil}
end

defp decode(_) do
:error
end

defp encode(value), do: value
end
1 change: 1 addition & 0 deletions lib/glimesh/api/scopes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Glimesh.Api.Scopes do
def authorize(:streamkey, %Access{} = ua, _params), do: scope_check(ua, :streamkey)
def authorize(:follow, %Access{} = ua, _params), do: scope_check(ua, :follow)
def authorize(:stream_info, %Access{} = ua, _params), do: scope_check(ua, :stream_info)
def authorize(:interactive, %Access{} = ua, _params), do: scope_check(ua, :interactive)

def authorize(:stream_mutations, %Access{is_admin: true}, _params) do
true
Expand Down
1 change: 1 addition & 0 deletions lib/glimesh/oauth/scopes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Glimesh.Oauth.Scopes do
"streamkey" -> gettext("scopestream")
"follow" -> gettext("scopefollow")
"stream_info" -> gettext("scopestream_info")
"interactive" -> gettext("scopeinteractive")
end
end
end
3 changes: 2 additions & 1 deletion lib/glimesh/oauth_migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ defmodule Glimesh.OauthMigration do
%{label: "Chat", name: "chat", public: true},
%{label: "Stream Key", name: "streamkey", public: true},
%{label: "Follow Channel", name: "follow", public: true},
%{label: "Update Stream Info", name: "stream_info", public: true}
%{label: "Update Stream Info", name: "stream_info", public: true},
%{label: "Verify Interactive Messages", name: "interactive", public: true}
]

Enum.each(scopes, fn attrs ->
Expand Down
1 change: 1 addition & 0 deletions lib/glimesh/streams.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ defmodule Glimesh.Streams do
def get_subscribe_topic(:viewers), do: "streams:viewers"
def get_subscribe_topic(:raid), do: "streams:raid"
def get_subscribe_topic(:channel, channel_id), do: "streams:channel:#{channel_id}"
def get_subscribe_topic(:interactive, channel_id), do: "streams:interactive:#{channel_id}"
def get_subscribe_topic(:stream, stream_id), do: "streams:stream:#{stream_id}"
def get_subscribe_topic(:chat, channel_id), do: "streams:chat:#{channel_id}"
def get_subscribe_topic(:chatters, channel_id), do: "streams:chatters:#{channel_id}"
Expand Down
6 changes: 5 additions & 1 deletion lib/glimesh/streams/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ defmodule Glimesh.Streams.Channel do
field :only_followed_can_raid, :boolean, default: false
field :raid_message, :string, default: "{streamer} is raiding you with {count} viewers!"

field :interactive_project, {:array, Glimesh.Interactive.Type}
field :interactive_enabled, :boolean, default: false

# This is here temporarily as we add additional schema to handle it.
field :streamloots_url, :string, default: nil

Expand Down Expand Up @@ -118,6 +121,7 @@ defmodule Glimesh.Streams.Channel do
:require_confirmed_email,
:minimum_account_age,
:allow_hosting,
:interactive_enabled,
:backend,
:allow_raiding,
:only_followed_can_raid,
Expand All @@ -133,7 +137,7 @@ defmodule Glimesh.Streams.Channel do
)
|> validate_inclusion(:backend, ["ftl", "whep"])
|> set_chat_rules_content_html()
|> cast_attachments(attrs, [:poster, :chat_bg])
|> cast_attachments(attrs, [:poster, :chat_bg, :interactive_project])
|> maybe_put_tags(:tags, attrs)
|> maybe_put_subcategory(:subcategory, attrs)
|> unique_constraint([:user_id])
Expand Down
32 changes: 32 additions & 0 deletions lib/glimesh_web/controllers/interactive_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule GlimeshWeb.InteractiveController do
use GlimeshWeb, :controller
alias Glimesh.ChannelLookups
alias Glimesh.Interactive

def index(conn, %{"username" => username}) do
# Get the channel, serve the project
channel = ChannelLookups.get_channel_for_username(username, true)

if channel.interactive_project do
# Serve the project
conn |> redirect(to: Interactive.url({"index.html", channel}, :request))
else
not_found(conn, nil)
end
end

def not_found(conn, _) do
# The project doesn't exist :(
conn
|> put_resp_header("Content-Type", "text/html")
|> send_file(200, "./priv/static/interactive/index.html")
end

def asset(conn, %{"id" => id, "asset" => asset}) do
# Get the channel
channel = ChannelLookups.get_channel_for_user_id(id, true)

# Serve the project asset
conn |> redirect(to: Interactive.url({Enum.join(asset, "/"), channel}, :request))
end
end
7 changes: 7 additions & 0 deletions lib/glimesh_web/live/user_live/stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ defmodule GlimeshWeb.UserLive.Stream do
|> assign(:player_error, nil)
|> assign(:user, maybe_user)
|> assign(:ultrawide, false)
|> assign(:interactive_toggle, false)}
|> assign(:webrtc_error, false)
|> assign(:can_raid, viewer_can_raid)
|> assign(:raid_starting, false)
Expand Down Expand Up @@ -301,6 +302,12 @@ defmodule GlimeshWeb.UserLive.Stream do
{:noreply, socket |> assign(:ultrawide, enabled)}
end

def handle_event("toggle_interactive", _value, socket) do
{:noreply,
socket
|> assign(:interactive_toggle, !socket.assigns.interactive_toggle)}
end

defp get_stream_thumbnail(channel) do
case channel.stream do
%Glimesh.Streams.Stream{} = stream ->
Expand Down
15 changes: 15 additions & 0 deletions lib/glimesh_web/live/user_live/stream.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@
session: %{"channel_id" => @channel.id}
) %>
</div>
<%= if @channel.interactive_enabled do %>
<div class="btn-group" role="group" aria-label="Fourth group">
<button
class="btn btn-info btn-responsive ml-1"
title="Toggle Interactive"
phx-click="toggle_interactive"
>
Interactive
</button>
</div>
<% end %>
</div>
</div>
</div>
Expand Down Expand Up @@ -227,6 +238,10 @@
data-rtrouter={Application.get_env(:glimesh, :rtrouter_url)}
>
</video>
<%= if @interactive_toggle do %>
<iframe sandbox="allow-scripts" src={"/#{@streamer.username}/interactive"}>
</iframe>
<% end %>
<div id="video-loading-container" class="">
<div class="lds-ring">
<div></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule GlimeshWeb.UserSettings.Components.ChannelSettingsLive do
use GlimeshWeb, :live_view

alias Glimesh.ChannelCategories
alias Glimesh.Interactive
alias Glimesh.Streams

@impl true
Expand Down Expand Up @@ -73,6 +74,15 @@ defmodule GlimeshWeb.UserSettings.Components.ChannelSettingsLive do
{:noreply, socket}
end

def handle_event("replace_interactive", _params, socket) do
#This has to run before a new project is uploaded. Removes the previous project
Enum.each(socket.assigns.channel.interactive_project, fn e ->
Interactive.delete({e.file_name, socket.assigns.channel})
end)

{:noreply, socket}
end

@impl true
def handle_event("rotate_stream_key", _params, socket) do
with :ok <-
Expand Down
Loading