Skip to content

Commit 10e9e37

Browse files
aMythoclone1018
andauthored
Add Interactive (#867)
* Add interactive fields to db * Add interactive to API * Allow user to upload interactive sessions * Display interactive project on channel, toggle it * Add interactive scope * Check scope before sending Authorized field * Upload and filter files * Remove zip of projects every 15 min * Check for executables * Remove old projects, cleanup conditions * Serve interactive project files * Add sandbox * Display errors if upload fails * Use file whitelist * Add code comments * Add basic default interactive project * Fix code quality * Switch to array from zip files * Only show itneractive button if its enabled * Fix code quality * Delete previous project before validation --------- Co-authored-by: Luke Strickland <luke@axxim.net>
1 parent 40b5f41 commit 10e9e37

File tree

22 files changed

+397
-5
lines changed

22 files changed

+397
-5
lines changed

lib/glimesh/api.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ defmodule Glimesh.Api do
2121
chat: false,
2222
streamkey: false,
2323
follow: false,
24-
stream_info: false
24+
stream_info: false,
25+
interactive: false
2526
}
2627
end
2728

lib/glimesh/api/resolvers/channel_resolver.ex

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Glimesh.Api.ChannelResolver do
22
@moduledoc false
33
import Ecto.Query
44

5+
alias Absinthe.Subscription
56
alias Glimesh.Api
67
alias Glimesh.ChannelCategories
78
alias Glimesh.ChannelLookups
@@ -275,6 +276,53 @@ defmodule Glimesh.Api.ChannelResolver do
275276
|> Api.connection_from_query_with_count(args)
276277
end
277278

279+
# Sends a message over client id
280+
def send_interactive_message(_parent, args, %{context: %{access: %{access_type: "app"}}}) do
281+
event_name = Map.get(args, :event_name)
282+
session = Map.get(args, :session_id)
283+
data = Map.get(args, :data)
284+
285+
Subscription.publish(
286+
GlimeshWeb.Endpoint,
287+
%{data: data, event_name: event_name, authorized: false},
288+
Keyword.put([], :interactive, "streams:interactive:#{session}")
289+
)
290+
291+
{:ok, %{data: data, event_name: event_name, authorized: false}}
292+
end
293+
294+
# Sends a message with an access token
295+
def send_interactive_message(_parent, args, %{context: %{access: access}}) do
296+
# Get the data from the message
297+
event_name = Map.get(args, :event_name)
298+
session = Map.get(args, :session_id)
299+
data = Map.get(args, :data)
300+
301+
# Check that it has interactive scope, channel matches sessionID
302+
# If so it is an authorized message, if not it isn't
303+
with true <- Map.get(access.scopes, :interactive),
304+
%Glimesh.Streams.Channel{} = channel <-
305+
ChannelLookups.get_channel_for_username(access.user.username),
306+
true <- channel.id == session do
307+
Subscription.publish(
308+
GlimeshWeb.Endpoint,
309+
%{data: data, event_name: event_name, authorized: true},
310+
Keyword.put([], :interactive, "streams:interactive:#{session}")
311+
)
312+
313+
{:ok, %{data: data, event_name: event_name, authorized: true}}
314+
else
315+
_ ->
316+
Subscription.publish(
317+
GlimeshWeb.Endpoint,
318+
%{data: data, event_name: event_name, authorized: false},
319+
Keyword.put([], :interactive, "streams:interactive:#{session}")
320+
)
321+
322+
{:ok, %{data: data, event_name: event_name, authorized: false}}
323+
end
324+
end
325+
278326
def get_moderation_logs(args, %{source: channel}) do
279327
Streams.ChannelModerationLog
280328
|> where(channel_id: ^channel.id)

lib/glimesh/api/schema/channel_types.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Glimesh.Api.ChannelTypes do
55

66
import Absinthe.Resolution.Helpers
77
import_types(Absinthe.Plug.Types)
8+
import_types(Glimesh.Api.Schema.Types.Interactive)
89

910
alias Glimesh.Api
1011
alias Glimesh.Api.ChannelResolver
@@ -107,6 +108,15 @@ defmodule Glimesh.Api.ChannelTypes do
107108
resolve(&ChannelResolver.upload_stream_thumbnail/3)
108109
end
109110

111+
@desc "Send a message over the interactive session"
112+
field :send_interactive_message, type: :interactive do
113+
arg(:session_id, non_null(:integer))
114+
arg(:event_name, non_null(:string))
115+
arg(:data, non_null(:json), description: "JSON data to send")
116+
117+
resolve(&ChannelResolver.send_interactive_message/3)
118+
end
119+
110120
@desc "Update a channel's stream info"
111121
field :update_stream_info, type: :channel do
112122
arg(:channel_id, non_null(:id))
@@ -127,6 +137,16 @@ defmodule Glimesh.Api.ChannelTypes do
127137
end
128138
end)
129139
end
140+
141+
field :interactive, :interactive do
142+
arg(:session, non_null(:integer), description: "The interactive session ID")
143+
144+
config(fn args, _ ->
145+
case Map.get(args, :session) do
146+
session -> {:ok, topic: [Streams.get_subscribe_topic(:interactive, session)]}
147+
end
148+
end)
149+
end
130150
end
131151

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

158+
@desc "An interactive packet"
159+
object :interactive do
160+
field :event_name, :string, description: "The name of the event"
161+
field :data, :json, description: "The data sent across the connection"
162+
field :authorized, :boolean, description: "Was this an authorized message?"
163+
field :inserted_at, non_null(:naive_datetime), description: "Interactive creation date"
164+
field :updated_at, non_null(:naive_datetime), description: "Interactive updated date"
165+
end
166+
138167
@desc "Categories are the containers for live streaming content."
139168
object :category do
140169
field :id, :id, description: "Unique category identifier"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule Glimesh.Api.Schema.Types.Interactive do
2+
@moduledoc """
3+
The Json scalar type allows arbitrary JSON values to be passed in and out.
4+
Requires `{ :jason, "~> 1.1" }` package: https://github.com/michalmuskala/jason
5+
We use this to make sure the user is sending valid json in an interactive message
6+
"""
7+
use Absinthe.Schema.Notation
8+
9+
scalar :json, name: "JSON" do
10+
description("""
11+
JSON data surrounded by a string.
12+
"{
13+
\"test\": 123
14+
}"
15+
""")
16+
17+
serialize(&encode/1)
18+
parse(&decode/1)
19+
end
20+
21+
# Decods the json, returns it if valid. Fails if not
22+
@spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error
23+
@spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
24+
defp decode(%Absinthe.Blueprint.Input.String{value: value}) do
25+
case Jason.decode(value) do
26+
{:ok, result} -> {:ok, result}
27+
_ -> :error
28+
end
29+
end
30+
31+
defp decode(%Absinthe.Blueprint.Input.Null{}) do
32+
{:ok, nil}
33+
end
34+
35+
defp decode(_) do
36+
:error
37+
end
38+
39+
defp encode(value), do: value
40+
end

lib/glimesh/api/scopes.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ defmodule Glimesh.Api.Scopes do
1818
def authorize(:streamkey, %Access{} = ua, _params), do: scope_check(ua, :streamkey)
1919
def authorize(:follow, %Access{} = ua, _params), do: scope_check(ua, :follow)
2020
def authorize(:stream_info, %Access{} = ua, _params), do: scope_check(ua, :stream_info)
21+
def authorize(:interactive, %Access{} = ua, _params), do: scope_check(ua, :interactive)
2122

2223
def authorize(:stream_mutations, %Access{is_admin: true}, _params) do
2324
true

lib/glimesh/oauth/scopes.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule Glimesh.Oauth.Scopes do
1111
"streamkey" -> gettext("scopestream")
1212
"follow" -> gettext("scopefollow")
1313
"stream_info" -> gettext("scopestream_info")
14+
"interactive" -> gettext("scopeinteractive")
1415
end
1516
end
1617
end

lib/glimesh/oauth_migration.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ defmodule Glimesh.OauthMigration do
7070
%{label: "Chat", name: "chat", public: true},
7171
%{label: "Stream Key", name: "streamkey", public: true},
7272
%{label: "Follow Channel", name: "follow", public: true},
73-
%{label: "Update Stream Info", name: "stream_info", public: true}
73+
%{label: "Update Stream Info", name: "stream_info", public: true},
74+
%{label: "Verify Interactive Messages", name: "interactive", public: true}
7475
]
7576

7677
Enum.each(scopes, fn attrs ->

lib/glimesh/streams.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ defmodule Glimesh.Streams do
2929
def get_subscribe_topic(:viewers), do: "streams:viewers"
3030
def get_subscribe_topic(:raid), do: "streams:raid"
3131
def get_subscribe_topic(:channel, channel_id), do: "streams:channel:#{channel_id}"
32+
def get_subscribe_topic(:interactive, channel_id), do: "streams:interactive:#{channel_id}"
3233
def get_subscribe_topic(:stream, stream_id), do: "streams:stream:#{stream_id}"
3334
def get_subscribe_topic(:chat, channel_id), do: "streams:chat:#{channel_id}"
3435
def get_subscribe_topic(:chatters, channel_id), do: "streams:chatters:#{channel_id}"

lib/glimesh/streams/channel.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ defmodule Glimesh.Streams.Channel do
4646
field :only_followed_can_raid, :boolean, default: false
4747
field :raid_message, :string, default: "{streamer} is raiding you with {count} viewers!"
4848

49+
field :interactive_project, {:array, Glimesh.Interactive.Type}
50+
field :interactive_enabled, :boolean, default: false
51+
4952
# This is here temporarily as we add additional schema to handle it.
5053
field :streamloots_url, :string, default: nil
5154

@@ -118,6 +121,7 @@ defmodule Glimesh.Streams.Channel do
118121
:require_confirmed_email,
119122
:minimum_account_age,
120123
:allow_hosting,
124+
:interactive_enabled,
121125
:backend,
122126
:allow_raiding,
123127
:only_followed_can_raid,
@@ -133,7 +137,7 @@ defmodule Glimesh.Streams.Channel do
133137
)
134138
|> validate_inclusion(:backend, ["ftl", "whep"])
135139
|> set_chat_rules_content_html()
136-
|> cast_attachments(attrs, [:poster, :chat_bg])
140+
|> cast_attachments(attrs, [:poster, :chat_bg, :interactive_project])
137141
|> maybe_put_tags(:tags, attrs)
138142
|> maybe_put_subcategory(:subcategory, attrs)
139143
|> unique_constraint([:user_id])
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
defmodule GlimeshWeb.InteractiveController do
2+
use GlimeshWeb, :controller
3+
alias Glimesh.ChannelLookups
4+
alias Glimesh.Interactive
5+
6+
def index(conn, %{"username" => username}) do
7+
# Get the channel, serve the project
8+
channel = ChannelLookups.get_channel_for_username(username, true)
9+
10+
if channel.interactive_project do
11+
# Serve the project
12+
conn |> redirect(to: Interactive.url({"index.html", channel}, :request))
13+
else
14+
not_found(conn, nil)
15+
end
16+
end
17+
18+
def not_found(conn, _) do
19+
# The project doesn't exist :(
20+
conn
21+
|> put_resp_header("Content-Type", "text/html")
22+
|> send_file(200, "./priv/static/interactive/index.html")
23+
end
24+
25+
def asset(conn, %{"id" => id, "asset" => asset}) do
26+
# Get the channel
27+
channel = ChannelLookups.get_channel_for_user_id(id, true)
28+
29+
# Serve the project asset
30+
conn |> redirect(to: Interactive.url({Enum.join(asset, "/"), channel}, :request))
31+
end
32+
end

0 commit comments

Comments
 (0)