Skip to content

Commit

Permalink
Develop a hosting feature so streamers can recommend other channels t…
Browse files Browse the repository at this point in the history
…o their community when they are offline. (#786)

* Auto hosting feature.

* Fixed incorrect fontawesome icon reference and bootstrap color.

* Fixed issue where auto host job wasn't cleaning up hosting channels that have gone live.

* Flip time comparison

Co-authored-by: Luke Strickland <[email protected]>
  • Loading branch information
MemoryLeakDeath and clone1018 authored Jan 15, 2022
1 parent bd0d52b commit 0943340
Show file tree
Hide file tree
Showing 37 changed files with 2,595 additions and 50 deletions.
2 changes: 2 additions & 0 deletions assets/css/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ $fa-font-path: "/fa-fonts";
@import "glimesh/components/follow-button";
@import "glimesh/components/apex-charts";
@import "glimesh/components/video";
@import "glimesh/components/channel-lookup-typeahead";

@import "glimesh/pages/dmca";
@import "glimesh/pages/hosting.scss";

@include media-breakpoint-up(lg) {
.layout-spacing {
Expand Down
40 changes: 40 additions & 0 deletions assets/css/glimesh/components/channel-lookup-typeahead.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@mixin show-dropdown {
z-index: 9999;
display: block;
max-height: 250px;
overflow: auto;
}

.channel-typeahead {
position: relative;
&-dropdown {
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
margin-top: -2px;
> .list-group-item {
display: flex;
align-items: center;
padding: 12px 15px;
margin: 0;
opacity: 100;
&:hover {
background: blue;
cursor: pointer;
}
}
&:hover {
@include show-dropdown;
}
}
&-input {
position: relative;
&:focus {
+ .channel-typeahead-dropdown {
@include show-dropdown;
}
}
}
}
14 changes: 14 additions & 0 deletions assets/css/glimesh/pages/hosting.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.fixed-header-table {
table {
text-align: left;
position: relative;
border-collapse: collapse;
}
th, td {
padding: 0.25rem;
}
th {
position: sticky;
top: 0; /* Don't forget this, required for the stickiness */
}
}
2 changes: 2 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import InfiniteScroll from "./hooks/InfiniteScroll";
import TagSearch from "./hooks/TagSearch";
import LaunchCountdown from "./hooks/LaunchCountdown";
import Tagify from "./hooks/Tagify";
import ChannelLookupTypeahead from "./hooks/ChannelLookupTypeahead";

// https://github.com/github/markdown-toolbar-element
import "@github/markdown-toolbar-element";
Expand All @@ -33,6 +34,7 @@ Hooks.InfiniteScroll = InfiniteScroll;
Hooks.TagSearch = TagSearch;
Hooks.LaunchCountdown = LaunchCountdown;
Hooks.Tagify = Tagify;
Hooks.ChannelLookupTypeahead = ChannelLookupTypeahead;

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let liveSocket = new LiveSocket("/live", Socket, {
Expand Down
11 changes: 11 additions & 0 deletions assets/js/hooks/ChannelLookupTypeahead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
mounted() {
this.el.addEventListener("click", e => {
let userId = this.el.getAttribute('data-id');
let userName = this.el.getAttribute('data-name');
let fieldName = this.el.getAttribute('data-fieldname');
let channelId = this.el.getAttribute('data-channel-id');
this.pushEvent(fieldName + "_selection_made", {"user_id": userId, "username": userName, "channel_id": channelId});
});
}
}
4 changes: 4 additions & 0 deletions lib/glimesh/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,8 @@ defmodule Glimesh.Accounts do
valid?: false
}}
end

def get_account_age_in_days(%User{} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.inserted_at, :second) / 86_400
end
end
139 changes: 139 additions & 0 deletions lib/glimesh/channel_hosts_lookups.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
defmodule Glimesh.ChannelHostsLookups do
@moduledoc """
Channel Hosting lookups
"""

import Ecto.Query, warn: false

alias Glimesh.ChannelLookups
alias Glimesh.Repo
alias Glimesh.Streams.ChannelHosts

def get_channel_hosting_list(channel_id) do
ChannelHosts
|> where([ch], ch.hosting_channel_id == ^channel_id)
|> preload(host: [:user], target: [:user])
|> Repo.all()
end

def get_current_hosting_target(channel) do
ChannelHosts
|> where([ch], ch.hosting_channel_id == ^channel.id and ch.status == "hosting")
|> limit(1)
|> preload(host: [:user], target: [:user])
|> Repo.one()
end

def get_targets_host_info(host_username, target_channel) do
host = ChannelLookups.get_channel_for_username(host_username)

ChannelHosts
|> where(
[ch],
ch.hosting_channel_id == ^host.id and ch.status == "hosting" and
ch.target_channel_id == ^target_channel.id
)
|> preload(host: [:user], target: [:user])
|> Repo.one()
end

@doc """
Used by Auto Host Job -- find channels that are being hosted but are no longer live and reset them to "ready" state
"""
def unhost_channels_not_live do
sql = """
update channel_hosts\
set status = 'ready'\
where id in\
(select ch.id from channel_hosts as ch\
inner join channels as tc on ch.target_channel_id = tc.id\
inner join channels as hc on ch.hosting_channel_id = hc.id\
where (tc.status != 'live' or hc.status = 'live')\
and ch.status = 'hosting')\
returning id\
"""

Repo.query(sql)
end

@doc """
Used by Auto Host Job -- find channels that are no longer eligible for hosting by the current host and set them to "error" state
"""
def invalidate_hosting_channels_where_necessary do
sql = """
update channel_hosts\
set status = 'error'\
where id in\
(select ch.id from channel_hosts as ch\
inner join channels as tc on ch.target_channel_id = tc.id\
inner join users as tu on tc.user_id = tu.id\
inner join channels as hc on ch.hosting_channel_id = hc.id\
inner join users as hu on hc.user_id = hu.id\
where ch.status != 'error'\
and (tc.allow_hosting = 'false'\
or tc.inaccessible = 'true'\
or tu.is_banned = 'true'\
or tu.can_stream = 'false'\
or exists(select user_id from channel_bans where user_id = hc.user_id and channel_id = tc.id and expires_at is null)\
or hu.is_banned = 'true'\
or hu.can_stream = 'false'\
or hc.inaccessible = 'true'))\
returning id\
"""

Repo.query(sql)
end

@doc """
Used by Auto Host Job -- find channels that are live with hosted channels that aren't live and host them!
This will only affect channel_hosts table records in "ready" state -- it is assumed that invalidate_hosting_channels_where_necessary()
and unhost_channels_not_live() have updated the channel_hosts table record states appropriately.
"""
def host_some_channels do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)

sql = """
update channel_hosts\
set status = 'hosting', last_hosted_date = $1\
where id in\
(select distinct on(ch_sub.hosting_channel_id) ch_sub.id\
from channel_hosts as ch_sub\
inner join channels as tc on ch_sub.target_channel_id = tc.id\
inner join channels as hc on ch_sub.hosting_channel_id = hc.id\
where ch_sub.status = 'ready'\
and hc.status = 'offline'\
and tc.status = 'live'\
and not exists(select true from channel_hosts where status = 'hosting' and hosting_channel_id = ch_sub.hosting_channel_id)\
group by ch_sub.id\
order by ch_sub.hosting_channel_id asc, min(coalesce(ch_sub.last_hosted_date, to_timestamp(0))) asc)\
returning id\
"""

Repo.query(sql, [now])
end

def recheck_error_status_channels do
sql = """
update channel_hosts\
set status = 'ready'\
where id in\
(select ch.id from channel_hosts as ch\
inner join channels as tc on ch.target_channel_id = tc.id\
inner join users as tu on tc.user_id = tu.id\
inner join channels as hc on ch.hosting_channel_id = hc.id\
inner join users as hu on hc.user_id = hu.id\
where ch.status = 'error'\
and tc.allow_hosting = 'true'\
and tc.inaccessible = 'false'\
and tu.is_banned = 'false'\
and tu.can_stream = 'true'\
and not exists(select user_id from channel_bans where user_id = hc.user_id and channel_id = tc.id and expires_at is null)\
and hu.is_banned = 'false'\
and hu.can_stream = 'true'\
and hc.inaccessible = 'false')\
returning id\
"""

Repo.query(sql)
end
end
92 changes: 91 additions & 1 deletion lib/glimesh/channel_lookups.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Glimesh.ChannelLookups do
alias Glimesh.AccountFollows.Follower
alias Glimesh.Accounts.User
alias Glimesh.Repo
alias Glimesh.Streams.{Category, Channel}
alias Glimesh.Streams.{Category, Channel, ChannelHosts}

## Filtering
@spec search_live_channels(map) :: list
Expand Down Expand Up @@ -113,6 +113,62 @@ defmodule Glimesh.ChannelLookups do
|> Repo.preload([:category, :user, :stream, :subcategory, :tags])
end

def count_live_followed_channels_that_are_hosting(%User{} = user) do
from(c in Channel,
left_join: f in Follower,
on: c.user_id == f.streamer_id,
join: ch in ChannelHosts,
on: c.id == ch.hosting_channel_id,
join: target in Channel,
on: target.id == ch.target_channel_id,
left_join: target_followers in Follower,
on: target.user_id == target_followers.streamer_id,
where: ch.status == "hosting",
where: target.status == "live",
where: f.user_id == ^user.id,
where: target_followers.user_id != ^user.id or is_nil(target_followers.user_id),
distinct: target.id,
select: [target]
)
|> Repo.all()
|> length()
end

def list_live_followed_channels_and_hosts(%User{} = user) do
include_hosts_query =
from(c in Channel,
left_join: f in Follower,
on: c.user_id == f.streamer_id,
join: ch in ChannelHosts,
on: c.id == ch.hosting_channel_id,
join: target in Channel,
on: target.id == ch.target_channel_id,
left_join: target_followers in Follower,
on: target.user_id == target_followers.streamer_id,
where: ch.status == "hosting",
where: target.status == "live",
where: f.user_id == ^user.id,
where: target_followers.user_id != ^user.id or is_nil(target_followers.user_id),
distinct: target.id,
select: [target],
select_merge: %{match_type: "hosting"}
)

live_followed_query =
from([c] in Channel,
join: f in Follower,
on: c.user_id == f.streamer_id,
where: c.status == "live",
where: f.user_id == ^user.id,
select_merge: %{match_type: "live"}
)

query = live_followed_query |> union_all(^include_hosts_query)

Repo.all(query)
|> Repo.preload([:user, :category, :stream, :subcategory, :tags])
end

def list_all_followed_channels(user) do
Repo.all(
from c in Channel,
Expand Down Expand Up @@ -202,4 +258,38 @@ defmodule Glimesh.ChannelLookups do
def get_any_channel_for_user(user) do
Repo.one(from c in Channel, where: c.user_id == ^user.id)
end

def search_hostable_channels_by_name(hosting_user, target_name) do
if target_name != nil and String.length(target_name) < 25 do
search_term = Regex.replace(~r/(\\\\|_|%)/, target_name, "\\\\\\1") <> "%"

Repo.all(
from c in Channel,
join: u in User,
on: c.user_id == u.id,
where: ilike(u.displayname, ^search_term),
where: c.allow_hosting == true,
where: c.inaccessible == false,
where: c.user_id != ^hosting_user.id,
where: u.is_banned == false,
where: u.can_stream == true,
where:
fragment(
"not exists(select user_id from channel_bans where user_id = ? and channel_id = ? and expires_at is null)",
^hosting_user.id,
c.id
),
where:
fragment(
"not exists(select target_channel_id from channel_hosts where target_channel_id = ?)",
c.id
),
order_by: [asc: u.displayname],
limit: 10
)
|> Repo.preload([:user])
else
[]
end
end
end
Loading

0 comments on commit 0943340

Please sign in to comment.