Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop a hosting feature so streamers can recommend other channels to their community when they are offline. #786

Merged
merged 4 commits into from
Jan 15, 2022

Conversation

MemoryLeakDeath
Copy link
Contributor

Summary

Hosting is a process by which a streamer may select other streams to promote to their community when their stream is offline. Typically the streamer will manage a list of other streams to promote ahead of time and the hosting process will commence without their direct intervention. This is in stark contrast to Raiding which must be initiated while live and will actively pull the viewers from the initiating live stream to the target live stream.

Feature Scope (Minimum Viable Feature)

  • This feature is not available to viewers. A user must have a channel to participate.
  • Streamers must be able to manage the list of channels they wish to host.
  • Streamers must meet minimum qualifications to host others. This is to not only make it more difficult for automated systems to create unwanted spam hosting but also to give the recipients of hosts options to block hosts who break their community's rules.
  • Viewers who land on a host's channel page will be redirected to the channel the streamer is hosting without the need for manual intervention. Viewers will also be presented with a message that the channel is being hosted to alleviate any confusion the sudden redirect may incur.
  • A channel that is live cannot host another channel until they go offline.
  • Hosting tends to be an invisible feature on most other streaming sites and is thus largely ineffectual. To help make this feature more useful (ie. visible) to the community, viewers who follow a channel that is hosting another will see those hosted channels in their "Following" page.
  • The act of hosting another channel is passive from the perspective of the streamers. This will necessitate the need for a background job to run periodically to manage this process.
  • Since hosting is automated and unconstrained by time periods, it should not participate in live notifications (viewers will probably not appreciate a message from us at 2am for a channel they don't directly follow).

Features Outside Initial Scope

  • Chat notifications when a channel is hosted by another. This could have implications to the various chat bots the community's streamers use and thus should be a specific change with its own scope.
  • Hosting statistics to show how many times a stream was hosted. This was originally in-scope, but I am having difficulty understanding how the statistics are tabulated in the codebase. I will revisit this at a later date.
  • Moderators of hosting channels will redirect to the channel being hosted just like normal viewers (the channel owner does not redirect). This seems like a low priority right now but could be addressed in the future (especially if an editor role is introduced to the platform).
  • Stopping or clearing a host. We have a persistent issue on the platform where occasionally channels will "get stuck" in live status even though they are not live. If that stuck channel is being hosted it will not be un-hosted until its status returns to "offline". As of this initial release, hosting streamers do not have direct control over what channel the system picks as its hosting target from the list.
  • UI design work. I've done my best to keep the design of the feature consistent with the overall site design but there are rough edges (type-ahead field suggestions is one example) and it would not hurt to have someone with a better eye for UI/UX design to give this a once-over.
  • API hooks/Mobile hooks. These will have to be explored in the future as I will need to learn what is involved with that type of change.

New Channel Settings Page: Hosting

settings-section

settings-page

From the new hosting settings page, streamers can add/remove channels to host and choose whether or not to allow other streamers to host their channel. Hosting other channels does NOT require a streamer allow others to host them.

Hosting Qualifications

To host other channels a user must meet these qualifications:

  • They must have a channel that is active: (channel.inaccessible = false, channel.can_stream = true)
  • They must have streamed at least 10 hours: (streams.get_channel_hours() >= 10)
  • Their user account must be at least 5 days old: (accounts.get_account_age_in_days() >= 5; user.inserted_at >= 5 days ago)
  • They must not be banned from the platform: (user.is_banned = false)

Furthermore, a streamer can only select channels for hosting that meet these qualifications:

  • The streamer MUST NOT be banned from the target chat, but CAN be timed out (channel_ban.expires_at != nil)
  • The target must allow hosting (channel.allow_hosting = true)
  • The target must have an active channel: (channel.inaccessible = false, channel.can_stream = true)
  • The target must not be banned from the platform: (user.is_banned = false)

NOTE: The auto hosting job will check these qualifications on each run and should handle scenarios where a host or target is temporarily disqualified (for instance, banned then un-banned) without intervention from the streamers.

Allow hosting toggle

"Other streamers may host my channel" is a simple true/false toggle that will save automatically upon change. It defaults to not allow (false) so all new and existing streamers will have to turn it on if they so desire. It affects a new column added to the "channels" database table ("allow_hosting", boolean, nullable: true, default: false)

Add channel field/button

Streamers can type/paste the name of another channel into the add channel field then click the button to attempt to add that channel to their hosting list. The add may fail if the channel in question does not meet the hosting qualifications listed in the previous section.

typeahead

The input field supports case-insensitive, type-ahead functionality and will provide up to 10 suggestions for channels whose name starts with the text input, meet the hosting qualifications specified above, and are not already on the streamer's hosting list. The maximum length of the input field is 24 since that is the maximum username/displayname size enforced by the system when creating users. The input field is sanitized against the following characters that have special meaning in an "ilike" sql statement: ('\', '_', '%') Though security researchers do not recommend using "like" statements, attempting to sanitize the postgres text search mechanisms looked far more daunting. My hope is the 24 character max length, the sanitization regex, and the use of sql replacement tokens will be sufficient to curtail sql injection attempts against this field.

Development notes

The Typeahead field is separated into it's own live_component so it can potentially be re-used in future pages. It is composed of the following files: "channel_lookup_typeahead.ex", "channel-lookup-typeahead.scss", and "ChannelLookupTypeahead.js".

Hosting channel list

hosting-channels-list

The hosting channel list displays what channels the streamer has selected for their hosting rotation, the status of the hosting target, the last time the target was hosted by the system, and a remove button to remove the target from the list.

Status codes

status-ready = "ready"; The target channel is in the hosting rotation.
status-hosting = "hosting"; The target channel is currently being hosted. A maximum of one such target can be in this status.
status-error = "error"; The target channel no longer qualifies for hosting and is not in rotation. This can be a temporary status as it is based on all the hosting qualification checks enumerated earlier in this document and will be re-checked every time the auto-hosting background job runs.

Last hosted and selection of next channel to host

The auto host job selects the next channel to host from the hosting list as follows:

  • Is the hosting channel offline (channel.status = "offline")?
  • Is the target in "ready" status (channel_hosts.status = "ready")?
  • Is the target channel live (channel.status = "live")?
  • Select the target channel with the longest amount of time since last hosted (order by: channel_hosts.last_hosted, asc)
  • Target channels that have never been hosted by the streamer take priority (channel_hosts.last_hosted is nil => 1/1/1970 00:00:00)
  • In the event of a tie, whichever database record is delivered to us first wins (the outcome can be random).

Development notes

This list is stored in a new database table with the following structure:

channel_hosts_table

Being Hosted, Channel Page

Viewers who arrive on an offline channel page that is hosting another channel will be automatically redirected to the hosted channel and presented with a message at the top of the screen informing them that the channel they originally went to is hosting another channel. Viewers can dismiss the message by clicking the "x" icon at the top right of the message box. Viewers can return to the hosting channel by clicking the "Return to host" button. The message has the following format:
"{host avatar} {host channel name} is hosting {target channel name} {target channel avatar}"

being-hosted

Hosting Another Channel, Channel Page

The owner of the hosting channel, viewers who have clicked "Return to Host", or viewers that have followed a link to a host's channel page that contains "?follow_host=false" at the end of the URL will be presented with a message that this channel is hosting another channel. Viewers may then click the "Go There" button to go to the stream being hosted or can dismiss the message by clicking the "x" at the top-right corner of the message box.

hosting

The channel owner will not automatically redirect to a channel they are hosting as this would be inconvenient if they are prepping to go live or change their stream title/tags/category (channel moderators WILL redirect in this current iteration). A channel owner that wishes to advertise a link to their channel (for instance in a pinned tweet or a linktr.ee page) can add "?follow_host=false" to the end of their channel URL so visitors will not automatically redirect to a channel being hosted. For example:

https://glimesh.tv/memtest86/?follow_host=false

Development Notes

  • The URL on the hosting target page has "?host=username" appended to it. I've tried to stay clear of putting primary key ids in the URL as that is generally frowned upon in the security community. Since the username is already a known entity used in channel URLs (and not a primary key), I felt that was secure enough.
  • I check whether the channel is hosting in the initial mount of the page then do a hard 302 redirect from there when appropriate. I felt this was a more straightforward and reliable way of handling this then attempting a push_redirect inline and hoping the page profile, support modal, stream, and chat channel would change appropriately.

The Following Page

Viewers who follow channels that are hosting other channels will now see those hosted channels in their following page with a special "Hosted" badge on them. This is in addition to seeing live channels they follow.

following-live

The database query that populates this page attempts to do the following:

  • Remove duplicates. If a viewer directly follows "Channel A" which is live and "Channel B" which is not live but is hosting "Channel A", they should only see "Channel A" on the following page (without the "hosted" badge).
  • Prefer directly followed live channels over hosted ones. If a user follows two channels "Channel Z" which is live and "Channel K" which is hosting "Channel J", "Channel Z" should show first on the following page and "Channel J" should appear last.

The Following Badge (count)

Viewers will see a notification badge next to the "Following" category at the top of the page when one or more channels they are following are live. This "call to action" is presented as a white number on a red background.

following-badge-live

In an effort to find a middle ground where we can promote hosted streams without making the following badge too noisy (thus prompting users to ignore it), I've added a white number on blue badge case. This number will ONLY appear when all the channels a viewer is following are offline but at least some of them are hosting other channels. Similar to the red badge, the blue badge number reflects the total number of hosted channels.

following-badge-not-live

following-hosted

Auto Host Job

The linchpin of the hosting feature is the auto hosting job. This job utilizes Rihanna to run every 10 minutes, so streamers should not expect to see their channel begin to host others immediately upon going offline. That said, viewers will no longer be redirected to a host-ed channel when the host-ing channel goes live (even though it may take up to 10 minutes for the database to reflect that the channel is no longer host-ing another). This job has the following tasks to perform in order:

  1. Check all hosting targets that are in error status (channel_hosts.status = "error") and see if they re-qualify for hosting and can return to "ready" status.
  2. Un-host any target channels that are no longer live (channel.status = "offline") or are the target of host channels that are now live (channel.status = "live").
  3. Check all hosting targets that are in ready status (channel_hosts.status = "ready") and see if they still qualify for hosting. If they no longer qualify, set them to error status.
  4. Look at all channels currently live (channel.status = "live"), determine if they are host targets, and then select channels to host them that are not currently hosting other channels.

After each step above, the job will write an info level log with the number of records updated. I decided to keep these update statements as raw sql since they are fairly complex and were becoming hard to read in Ecto.Query form. The job does not attempt to retry if it fails.

Security Considerations

The following are points to consider when evaluating this feature from a security standpoint. I've made every effort to code in a secure manner but that does not mean I haven't goofed; especially given my inexperience with the Elixir language.

  • The add channel input field on the hosting settings page can potentially be subject to sql injection attacks. I do sanitize the input and use the standard Ecto query syntax to perform the "ilike" database operation.
  • I use Bodyguard to prevent anyone besides the channel owner from adding or removing hosted channel targets. My inexperience with Elixir though means that I don't know if I've implemented those security checks properly or completely.
  • The auto hosting background job does not take user input so it should be relatively safe.
  • A hosted channel will now show the host username in the URL query string. As this username is utilized in a standard channel URL I don't believe this will pose any additional security risk.

Performance Considerations

The following are points to consider when evaluating this feature from a performance standpoint. Whenever possible, I have offloaded much of the heavy lifting to the database. However, I only have a small development database with few records in it so I'm not able to really test this feature under load.

  • ChannelLookups.search_hostable_channels_by_name() will run for each character entered in the add channels text input field and utilizes two sub-queries with a limit of 10.
  • ChannelLookups.list_live_followed_channels_and_hosts() will run whenever a user goes to their "Following" page. It uses several joins and a "union all". On my local machine, postgres ranks the explain plan as between 12 and 46 but it seems to utilize only indexed and primary keys.
  • The following update queries are somewhat complex and are run by the auto host job every 10 minutes: ChannelHostsLookups.unhost_channels_not_live(), ChannelHostsLookups.invalidate_hosting_channels_where_necessary(), ChannelHostsLookups.recheck_error_status_channels(), ChannelHostsLookups.host_some_channels()

Additional Considerations

This is my first feature I've written in the Elixir language so a thorough code review would be much appreciated. Any feedback on code style/structure would also be welcome. I'm not completely unfamiliar with programming; I've worked professionally in Java for 18 years and 1 year in Ruby on Rails (and dabbled in other languages as hobby projects).

|> preload(host: [:user], target: [:user])
|> Repo.one()
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used raw sql in the following methods for clarity purposes. The Ecto.Query format was getting hard to read.

|> Repo.all()
|> length()
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noted the following method in my "Performance Considerations" section. This query mostly utilizes joins on indexes but I have not been able to test it with a large dataset.

@@ -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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following method is used by the add hosts text input box type-ahead code. Since it takes raw user input, I attempt to sanitize that input against '\', '_', and '%' characters.

@@ -44,6 +46,9 @@ defmodule Glimesh.Streams.Channel do
field :poster, Glimesh.ChannelPoster.Type
field :chat_bg, Glimesh.ChatBackground.Type

# This is used when searching for live channels that are live or hosted
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use this virtual field on the "Following" page to determine if the matching stream was the result of the user directly following the live stream or the result of the user following an offline channel that is hosting a live stream. Basically, this determines if I should put the "Hosted" gold badge on the stream's image on the page.

socket
|> assign(:channel, channel)
|> assign(:allow_changeset, Channel.change_allow_hosting(channel))
|> put_flash(:hosting_info, gettext("Saved Hosting Preference."))}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use "hosting_info" instead of just "info" for the flash messages due to a bug I've reported: 785

@@ -2,14 +2,14 @@
<div class="position-relative overflow-hidden p-3 p-md-5 m-md-3 text-center">
<div class="col-md-12 mx-auto ">
<h1 class="display-4 font-weight-normal">
<%= gettext("Live Followed Streams") %>
<%= gettext("Live Streams") %>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word "Followed" here seemed redundant so I removed it.

</h1>
<%= if length(@channels) == 0 do %>
<p><%= gettext("None of the streams you follow are live.") %></p>
<% end %>
</div>
</div>
<div class="row">
<div class="row d-flex justify-content-center">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added "d-flex" and "justify-content-center" so the stream cards will now align with the center of the page if there are less then three on a row. The previous left-justification felt a bit ugly to me.

:redirect_to_hosted_target => redirect_to_hosted_target,
:hosting_channel => hosting_channel
}} = get_hosting_data(params, channel, maybe_user, streamer)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped the existing mount code in an if statement to check if we should redirect immediately if hosting another channel.

@@ -1,8 +1,9 @@
<!-- app.html.eex -->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found it saves debugging effort later on to wrap shared html code snippets in a comment that describes where it comes from.

@MemoryLeakDeath MemoryLeakDeath marked this pull request as ready for review January 4, 2022 06:14
@clone1018
Copy link
Member

Hey @MemoryLeakDeath, amazing work on this! I've reviewed the PR description so far and everything seems like a very good approach. I'm going to start looking over the code and testing it out locally, and will let you know later this week what feedback I have on it! The PR has also come at a great time, as we're not working on any major rewrites that would render this PR stale, so I hope we can get it through the pipeline asap!

@MemoryLeakDeath
Copy link
Contributor Author

MemoryLeakDeath commented Jan 5, 2022 via email

@clone1018
Copy link
Member

Assuming you have a full dev instance running, you can run Rihanna jobs locally (and they'll keep registering themselves to run) by entering an iex shell with iex -S mix inside the project directory, and then running Rihanna.schedule(Glimesh.Jobs.AutoHostCron, [], in: 1) to schedule the job in 1 second.

@MemoryLeakDeath
Copy link
Contributor Author

MemoryLeakDeath commented Jan 5, 2022 via email

@clone1018 clone1018 merged commit 0943340 into Glimesh:dev Jan 15, 2022
@clone1018
Copy link
Member

Thanks for the work! It'll be on production soon.

@clone1018 clone1018 mentioned this pull request Jan 15, 2022
@MemoryLeakDeath
Copy link
Contributor Author

MemoryLeakDeath commented Jan 15, 2022 via email

@MemoryLeakDeath MemoryLeakDeath deleted the feature/hosting branch March 3, 2022 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants