├── .nvmrc
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── main.yml
├── .tool-versions
├── assets
├── .prettierrc.json
├── js
│ ├── channels
│ │ ├── index.js
│ │ └── captions.js
│ ├── utils
│ │ ├── index.js
│ │ ├── capitalize.js
│ │ ├── graphql.js
│ │ ├── event-emitter.js
│ │ └── browser_compatibility.js
│ ├── service
│ │ ├── app-client.js
│ │ ├── socket.js
│ │ ├── zoom-sequence.js
│ │ └── deepgram.js
│ ├── tailwind.js
│ ├── controllers
│ │ ├── dropdown_controller.js
│ │ ├── index.js
│ │ ├── darkmode_controller.js
│ │ ├── translations_controller.js
│ │ └── zoom_controller.js
│ ├── init_toast.js
│ └── stimulus.js
├── css
│ └── app.css
├── postcss.config.js
├── .eslintrc
└── tailwind.config.js
├── elixir_buildpack.config
├── phoenix_static_buildpack.config
├── lib
├── stream_closed_captioner_phoenix_web
│ ├── components
│ │ ├── layouts
│ │ │ ├── email.text.heex
│ │ │ ├── app.html.heex
│ │ │ ├── _notification.html.heex
│ │ │ ├── live.html.heex
│ │ │ ├── transcript.html.heex
│ │ │ ├── root.html.heex
│ │ │ ├── session.html.heex
│ │ │ └── _user_dropdown.html.heex
│ │ ├── layouts.ex
│ │ ├── cards.ex
│ │ ├── component_library.ex
│ │ └── dropdowns.ex
│ ├── controllers
│ │ ├── email
│ │ │ └── welcome.html.heex
│ │ ├── stream_settings_html.ex
│ │ ├── email_html.ex
│ │ ├── terms_html.ex
│ │ ├── privacy_html.ex
│ │ ├── user_session_html.ex
│ │ ├── user_settings_html.ex
│ │ ├── announcements_html.ex
│ │ ├── bits_balance_debit_html.ex
│ │ ├── user_confirmation_html.ex
│ │ ├── user_registration_html.ex
│ │ ├── user_reset_password_html.ex
│ │ ├── terms_controller.ex
│ │ ├── privacy_controller.ex
│ │ ├── showcase_html.ex
│ │ ├── dashboard_html.ex
│ │ ├── supporters_controller.ex
│ │ ├── bits_balance_debit
│ │ │ ├── show.html.heex
│ │ │ └── index.html.heex
│ │ ├── user_confirmation
│ │ │ └── new.html.heex
│ │ ├── announcements_controller.ex
│ │ ├── dashboard
│ │ │ ├── switch
│ │ │ │ ├── _switch_off.html.eex
│ │ │ │ └── _switch_on.html.eex
│ │ │ └── indicators
│ │ │ │ └── twitch_enabled_indicator.html.heex
│ │ ├── showcase_controller.ex
│ │ ├── error_html.ex
│ │ ├── bits_balance_debit_controller.ex
│ │ ├── supporters_html.ex
│ │ ├── dashboard_controller.ex
│ │ ├── user_reset_password
│ │ │ ├── new.html.heex
│ │ │ └── edit.html.heex
│ │ ├── user_registration
│ │ │ └── _twitch_connect_button.html.heex
│ │ ├── stream_settings_controller.ex
│ │ ├── user_registration_controller.ex
│ │ ├── error
│ │ │ ├── 500.html.heex
│ │ │ └── 404.html.heex
│ │ ├── announcements
│ │ │ └── index.html.heex
│ │ ├── supporters
│ │ │ └── index.html.heex
│ │ ├── showcase
│ │ │ └── index.html.heex
│ │ ├── error_helpers.ex
│ │ ├── user_confirmation_controller.ex
│ │ ├── user_reset_password_controller.ex
│ │ └── user_session_controller.ex
│ ├── heart_check.ex
│ ├── body_reader.ex
│ ├── plugs
│ │ ├── auth_access_pipeline.ex
│ │ ├── auth_error_handler.ex
│ │ ├── maintenance.ex
│ │ └── context.ex
│ ├── resolvers
│ │ ├── accounts_oauth.ex
│ │ ├── settings.ex
│ │ ├── accounts.ex
│ │ └── bits.ex
│ ├── gettext.ex
│ ├── schema
│ │ ├── middleware
│ │ │ └── authorized_introspection.ex
│ │ ├── accounts_types.ex
│ │ └── types
│ │ │ └── custom
│ │ │ ├── json_type.ex
│ │ │ └── datetime_type.ex
│ ├── live
│ │ ├── live_helpers.ex
│ │ ├── page_live.ex
│ │ ├── modal_component.ex
│ │ ├── caption_settings_live
│ │ │ └── form_component.ex
│ │ └── transcirpts_live
│ │ │ └── show.html.heex
│ ├── gql_config.ex
│ └── channels
│ │ ├── active_presence.ex
│ │ └── user_socket.ex
├── stream_closed_captioner_phoenix
│ ├── services
│ │ ├── twitch_bot.ex
│ │ ├── twitch
│ │ │ ├── extension
│ │ │ │ ├── token.ex
│ │ │ │ ├── credentials.ex
│ │ │ │ └── captions_payload.ex
│ │ │ ├── helix
│ │ │ │ ├── cost.ex
│ │ │ │ ├── credentials.ex
│ │ │ │ ├── extension_info.ex
│ │ │ │ ├── extension_channel.ex
│ │ │ │ ├── product_data.ex
│ │ │ │ ├── eventsub.ex
│ │ │ │ ├── user.ex
│ │ │ │ ├── stream.ex
│ │ │ │ └── transaction.ex
│ │ │ ├── extension_provider.ex
│ │ │ ├── parser.ex
│ │ │ ├── http_helpers.ex
│ │ │ ├── extension.ex
│ │ │ ├── oauth.ex
│ │ │ ├── helix_provider.ex
│ │ │ └── jwt.ex
│ │ ├── notion.ex
│ │ ├── zoom
│ │ │ └── params.ex
│ │ ├── azure
│ │ │ ├── cognitive_provider.ex
│ │ │ ├── cognitive
│ │ │ │ ├── translation.ex
│ │ │ │ └── translations.ex
│ │ │ └── cognitive.ex
│ │ ├── notion
│ │ │ ├── block.ex
│ │ │ ├── page.ex
│ │ │ ├── utils.ex
│ │ │ ├── parser.ex
│ │ │ └── base.ex
│ │ ├── helpers.ex
│ │ ├── azure.ex
│ │ ├── gooseman_app.ex
│ │ └── zoom.ex
│ ├── mailer.ex
│ ├── cache.ex
│ ├── repo.ex
│ ├── announcement_admin.ex
│ ├── announcement.ex
│ ├── guardian.ex
│ ├── bits
│ │ ├── bits_balance_queries.ex
│ │ ├── bits_balance_debit_admin.ex
│ │ ├── bits_balance_admin.ex
│ │ ├── bits_balance_debit.ex
│ │ ├── bits_transaction_admin.ex
│ │ ├── bits_balance.ex
│ │ ├── bits_transaction.ex
│ │ ├── bits_balance_debit_queries.ex
│ │ └── bits_transaction_queries.ex
│ ├── settings
│ │ ├── translate_languages_admin.ex
│ │ ├── translate_language.ex
│ │ └── stream_settings_admin.ex
│ ├── accounts
│ │ ├── eventsub_subscription.ex
│ │ ├── eventsub_subscription_admin.ex
│ │ ├── eventsub_subscription_queries.ex
│ │ ├── user_queries.ex
│ │ └── user_notifier.ex
│ ├── release.ex
│ ├── transcripts
│ │ ├── message.ex
│ │ ├── transcript.ex
│ │ └── transcript_admin.ex
│ ├── jobs
│ │ └── send_chat_reminder.ex
│ ├── emails.ex
│ ├── types
│ │ └── deepgram.ex
│ └── captions_pipeline
│ │ └── translations.ex
└── stream_closed_captioner_phoenix.ex
├── priv
├── repo
│ ├── migrations
│ │ ├── .formatter.exs
│ │ ├── 20220914051536_create_oban_peers.exs
│ │ ├── 20230105053116_create_announcements.exs
│ │ ├── 20210615041739_add_access_token_to_users_table.exs
│ │ ├── 20210803024103_add_blocklist_to_users_tables.exs
│ │ ├── 20221218174655_add_auto_off_captions_to_user_settings.exs
│ │ ├── 20211221032820_add_turn_on_reminder_to_stream_settings.exs
│ │ ├── 20211102040347_add_oban_jobs_table.exs
│ │ ├── 20211221041043_create_eventsub_subscriptions.exs
│ │ ├── 20201205194222_create_users_auth_tables.exs
│ │ ├── 20220914052253_swap_primary_oban_indexes.exs
│ │ └── 20210227232918_create_feature_flags_table.exs
│ └── seeds.exs
├── static
│ ├── images
│ │ ├── phoenix.png
│ │ ├── cc100x100.png
│ │ ├── register.png
│ │ ├── settings.png
│ │ ├── user-outline.png
│ │ ├── captions-start.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── install-extension.png
│ │ └── patreon.svg
│ └── cache_manifest.json
└── templates
│ ├── phx.gen.pretty_html
│ ├── view.ex
│ ├── new.html.eex
│ ├── edit.html.eex
│ ├── form.html.eex
│ ├── show.html.eex
│ └── index.html.eex
│ └── phx.gen.pretty_live
│ ├── form_component.html.leex
│ ├── show.ex
│ ├── live_helpers.ex
│ ├── modal_component.ex
│ ├── show.html.leex
│ └── index.ex
├── rel
└── overlays
│ ├── bin
│ ├── server.bat
│ ├── migrate.bat
│ ├── server
│ └── migrate
│ └── Procfile
├── jsconfig.json
├── config
├── profanity
│ └── english.txt
├── prod.exs
└── dev.secret.exs
├── .formatter.exs
├── Dockerrun.aws.json
├── test
├── stream_closed_captioner_phoenix
│ ├── captions_pipeline
│ │ └── profanity_test.exs
│ └── jobs
│ │ └── send_chat_reminder_test.exs
├── stream_closed_captioner_phoenix_web
│ ├── views
│ │ ├── layout_view_test.exs
│ │ └── error_view_test.exs
│ ├── live
│ │ ├── page_live_test.exs
│ │ └── stream_settings_live_test.exs
│ ├── channels
│ │ ├── captions_channel_test.exs
│ │ └── user_tracker_test.exs
│ └── controllers
│ │ └── bits_balance_debit_controller_test.exs
├── test_helper.exs
└── support
│ ├── fixtures
│ ├── accounts_fixtures.ex
│ ├── transcripts_fixtures.ex
│ ├── bits_fixture.ex
│ └── settings_fixture.ex
│ ├── channel_case.ex
│ └── data_case.ex
├── .vscode
├── settings.json
└── launch.json
├── .devcontainer
└── docker-compose.yml
├── README.md
├── .dockerignore
└── Dockerfile.prod
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.15.0
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: talk2megooseman
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | elixir 1.16.0
2 | erlang 26.0
3 |
--------------------------------------------------------------------------------
/assets/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false
3 | }
4 |
--------------------------------------------------------------------------------
/assets/js/channels/index.js:
--------------------------------------------------------------------------------
1 | export * from "./captions"
2 |
--------------------------------------------------------------------------------
/elixir_buildpack.config:
--------------------------------------------------------------------------------
1 | elixir_version=1.16.0
2 | erlang_version=26.0
3 |
--------------------------------------------------------------------------------
/phoenix_static_buildpack.config:
--------------------------------------------------------------------------------
1 | node_version=18.15.0
2 | clean_cache=true
3 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/email.text.heex:
--------------------------------------------------------------------------------
1 | <%= @inner_content %>
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/email/welcome.html.heex:
--------------------------------------------------------------------------------
1 |
Thanks for joining
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch_bot.ex:
--------------------------------------------------------------------------------
1 | defmodule TwitchBot do
2 | use TMI
3 | end
4 |
--------------------------------------------------------------------------------
/rel/overlays/bin/server.bat:
--------------------------------------------------------------------------------
1 | set PHX_SERVER=true
2 | call "%~dp0\stream_closed_captioner_phoenix" start
3 |
--------------------------------------------------------------------------------
/assets/css/app.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 | @import "tailwindcss/components";
3 | @import "tailwindcss/utilities";
4 |
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate.bat:
--------------------------------------------------------------------------------
1 | call "%~dp0\stream_closed_captioner_phoenix" eval StreamClosedCaptionerPhoenix.Release.migrate
2 |
--------------------------------------------------------------------------------
/rel/overlays/bin/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd -P -- "$(dirname -- "$0")"
3 | PHX_SERVER=true exec ./stream_closed_captioner_phoenix start
4 |
--------------------------------------------------------------------------------
/assets/js/utils/index.js:
--------------------------------------------------------------------------------
1 | export * from './browser_compatibility';
2 | export * from './event-emitter';
3 | export * from './capitalize';
4 |
--------------------------------------------------------------------------------
/assets/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('tailwindcss'),
4 | require('autoprefixer'),
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/extension/token.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Extension.Token do
2 | use Joken.Config
3 | end
4 |
--------------------------------------------------------------------------------
/priv/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/phoenix.png
--------------------------------------------------------------------------------
/priv/static/images/cc100x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/cc100x100.png
--------------------------------------------------------------------------------
/priv/static/images/register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/register.png
--------------------------------------------------------------------------------
/priv/static/images/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/settings.png
--------------------------------------------------------------------------------
/priv/static/images/user-outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/user-outline.png
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion do
2 | @moduledoc """
3 | Notion API
4 | ## Usage
5 |
6 | """
7 | end
8 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/zoom/params.ex:
--------------------------------------------------------------------------------
1 | defmodule Zoom.Params do
2 | defstruct [:seq, :lang]
3 |
4 | use ExConstructor
5 | end
6 |
--------------------------------------------------------------------------------
/priv/static/images/captions-start.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/captions-start.png
--------------------------------------------------------------------------------
/priv/static/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/favicon-16x16.png
--------------------------------------------------------------------------------
/priv/static/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/favicon-32x32.png
--------------------------------------------------------------------------------
/priv/static/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/favicon-96x96.png
--------------------------------------------------------------------------------
/assets/js/service/app-client.js:
--------------------------------------------------------------------------------
1 | import { GraphQLClient } from "graphql-request";
2 |
3 | export const appClient = new GraphQLClient('/api', { headers: {} })
4 |
--------------------------------------------------------------------------------
/priv/static/images/install-extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/talk2MeGooseman/stream_closed_captioner_phoenix/HEAD/priv/static/images/install-extension.png
--------------------------------------------------------------------------------
/rel/overlays/Procfile:
--------------------------------------------------------------------------------
1 | web: /app/bin/$GIGALIXIR_APP_NAME eval "StreamClosedCaptionerPhoenix.Release.migrate" && /app/bin/$GIGALIXIR_APP_NAME $GIGALIXIR_COMMAND
2 |
--------------------------------------------------------------------------------
/rel/overlays/bin/migrate:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd -P -- "$(dirname -- "$0")"
3 | exec ./stream_closed_captioner_phoenix eval StreamClosedCaptionerPhoenix.Release.migrate
4 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Mailer do
2 | use Bamboo.Mailer, otp_app: :stream_closed_captioner_phoenix
3 | end
4 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": [
3 | "assets/node_modules",
4 | "**/node_modules/*"
5 | ],
6 | "include": [
7 | "assets/js/**/*.js"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/assets/js/utils/capitalize.js:
--------------------------------------------------------------------------------
1 | export function capitalize(sentence) {
2 | if (sentence.length === 0) return ''
3 |
4 | return sentence[0].toUpperCase() + sentence.substr(1)
5 | }
6 |
--------------------------------------------------------------------------------
/assets/js/utils/graphql.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'graphql-request'
2 |
3 | export const GET_ME = gql`
4 | {
5 | me {
6 | extensionInstalled
7 | }
8 | }
9 | `
10 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/heart_check.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.HeartCheck do
2 | use HeartCheck
3 |
4 | add :some_check do
5 | :ok
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/stream_settings_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.StreamSettingsHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | end
4 |
--------------------------------------------------------------------------------
/config/profanity/english.txt:
--------------------------------------------------------------------------------
1 | OMERGUDLUL123
2 | dyke
3 | fag
4 | faggot
5 | kike
6 | nigger
7 | wetback
8 | beaner
9 | chink
10 | coon
11 | coons
12 | coonnass
13 | rape
14 | retard
15 | retarded
16 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/email_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.EmailHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("email/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/terms_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.TermsHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("terms/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/cache.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Cache do
2 | use Nebulex.Cache,
3 | otp_app: :stream_closed_captioner_phoenix,
4 | adapter: Nebulex.Adapters.Local
5 | end
6 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/privacy_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.PrivacyHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("privacy/*")
4 | end
5 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/view.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>View do
2 | use <%= inspect context.web_module %>, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Layouts do
2 | @moduledoc false
3 | use StreamClosedCaptionerPhoenixWeb, :html
4 |
5 | embed_templates "layouts/*"
6 | end
7 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_session_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserSessionHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("user_session/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_settings_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserSettingsHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("user_settings/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/announcements_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.AnnouncementsHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 |
4 | embed_templates("announcements/*")
5 | end
6 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo do
2 | use EctoExtras.Repo
3 |
4 | use Ecto.Repo,
5 | otp_app: :stream_closed_captioner_phoenix,
6 | adapter: Ecto.Adapters.Postgres
7 | end
8 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/azure/cognitive_provider.ex:
--------------------------------------------------------------------------------
1 | defmodule Azure.CognitiveProvider do
2 | alias Azure.Cognitive.Translations
3 |
4 | @callback translate(String.t(), [String.t()], String.t()) :: Translations.t()
5 | end
6 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/bits_balance_debit_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.BitsBalanceDebitHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("bits_balance_debit/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_confirmation_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserConfirmationHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("user_confirmation/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_registration_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserRegistrationHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("user_registration/*")
4 | end
5 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_reset_password_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserResetPasswordHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("user_reset_password/*")
4 | end
5 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :ecto_sql, :phoenix],
3 | inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
4 | subdirectories: ["priv/*/migrations"],
5 | plugins: [Phoenix.LiveView.HTMLFormatter],
6 | ]
7 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/terms_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.TermsController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | def index(conn, _params) do
5 | render(conn, "index.html")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220914051536_create_oban_peers.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.CreateObanPeers do
2 | use Ecto.Migration
3 |
4 | def up, do: Oban.Migrations.up(version: 11)
5 |
6 | def down, do: Oban.Migrations.down(version: 11)
7 | end
8 |
--------------------------------------------------------------------------------
/assets/js/tailwind.js:
--------------------------------------------------------------------------------
1 | if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
2 | document.documentElement.classList.add('dark');
3 | } else {
4 | document.documentElement.classList.remove('dark');
5 | }
6 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/privacy_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.PrivacyController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | def index(conn, _params) do
5 | render(conn, "index.html")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Dockerrun.aws.json:
--------------------------------------------------------------------------------
1 | {
2 | "AWSEBDockerrunVersion": "1",
3 | "Image": {
4 | "Name": "ghcr.io/talk2megooseman/stream_closed_captioner_phoenix:latest"
5 | },
6 | "Ports": [
7 | {
8 | "ContainerPort": 4000,
9 | "HostPort": 80
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/cost.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.Cost do
2 | defstruct [
3 | :amount,
4 | :type
5 | ]
6 |
7 | @type t :: %__MODULE__{
8 | amount: non_neg_integer(),
9 | type: String.t()
10 | }
11 | use ExConstructor
12 | end
13 |
--------------------------------------------------------------------------------
/assets/js/service/socket.js:
--------------------------------------------------------------------------------
1 | import { Socket } from "phoenix"
2 |
3 | /**
4 | * The socket connection
5 | * @type {Socket}
6 | */
7 | let socket
8 | if (window.userToken) {
9 | socket = new Socket("/socket", { params: { token: window.userToken } })
10 | socket.connect()
11 | }
12 |
13 | export default socket
14 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/extension/credentials.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Extension.Credentials do
2 | @type t :: %__MODULE__{
3 | client_id: String.t(),
4 | token_secret: String.t(),
5 | jwt_token: any
6 | }
7 | defstruct [:client_id, :token_secret, :jwt_token]
8 | end
9 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/body_reader.ex:
--------------------------------------------------------------------------------
1 | defmodule BodyReader do
2 | def cache_raw_body(conn, opts) do
3 | with {:ok, body, conn} <- Plug.Conn.read_body(conn, opts) do
4 | conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
5 |
6 | {:ok, body, conn}
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion/block.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion.Block do
2 | @moduledoc """
3 | Simple API wrapper for Notion API
4 | """
5 | import Notion.Base
6 |
7 | @doc """
8 | """
9 | def retrieve_block_children(page_id, params),
10 | do: get("blocks/#{page_id}/children", params)
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230105053116_create_announcements.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.CreateAnnouncements do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:announcements) do
6 | add :message, :text
7 | add :display, :boolean, default: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Helpers do
2 | @spec encode_url_and_params(binary | URI.t(), map() | list()) :: binary
3 | def encode_url_and_params(url, params \\ %{}) do
4 | url
5 | |> URI.parse()
6 | |> Map.put(:query, URI.encode_query(params))
7 | |> URI.to_string()
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/credentials.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.Credentials do
2 | defstruct [:client_id, :client_secret, :access_token]
3 |
4 | @type t :: %__MODULE__{
5 | client_id: String.t(),
6 | client_secret: String.t(),
7 | access_token: String.t()
8 | }
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210615041739_add_access_token_to_users_table.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.AddAccessTokenToUsersTable do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:users) do
6 | add :access_token, :string
7 | add :refresh_token, :string
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210803024103_add_blocklist_to_users_tables.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.AddBlocklistToUsersTables do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:stream_settings) do
6 | add :blocklist, {:array, :string}, default: [], null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221218174655_add_auto_off_captions_to_user_settings.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.AddAutoOffCaptionsToUserSettings do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:stream_settings) do
6 | add :auto_off_captions, :boolean, default: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix/captions_pipeline/profanity_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.CaptionsPipeline.ProfanityTest do
2 | use StreamClosedCaptionerPhoenix.DataCase, async: true
3 |
4 | # This runs the tests using the doc header on the function
5 | doctest StreamClosedCaptionerPhoenix.CaptionsPipeline.Profanity
6 | end
7 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211221032820_add_turn_on_reminder_to_stream_settings.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.AddTurnOnReminderToStreamSettings do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:stream_settings) do
6 | add :turn_on_reminder, :boolean, default: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/assets/js/utils/event-emitter.js:
--------------------------------------------------------------------------------
1 | export const sendEvent = (name, data) => {
2 | const event = new CustomEvent(name, { detail: data })
3 |
4 | window.dispatchEvent(event)
5 | }
6 |
7 | export const onEvent = (name, callback) => {
8 | window.addEventListener(name, callback, false)
9 | return () => window.removeEventListener(name, callback)
10 | }
11 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/announcement_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.AnnouncementAdmin do
2 | def index(_) do
3 | [
4 | display: nil,
5 | message: nil
6 | ]
7 | end
8 |
9 | def form_fields(_) do
10 | [
11 | display: nil,
12 | message: %{type: :richtext}
13 | ]
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix do
2 | @moduledoc """
3 | StreamClosedCaptionerPhoenix keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/plugs/auth_access_pipeline.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.AuthAccessPipeline do
2 | use Guardian.Plug.Pipeline, otp_app: :stream_closed_captioner_phoenix
3 |
4 | plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
5 | plug Guardian.Plug.EnsureAuthenticated
6 | plug Guardian.Plug.LoadResource, allow_blank: true
7 | end
8 |
--------------------------------------------------------------------------------
/assets/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-base",
4 | "adjunct"
5 | ],
6 | "env": {
7 | "browser": true
8 | },
9 | "rules": {
10 | "no-plusplus": [
11 | "error",
12 | {
13 | "allowForLoopAfterthoughts": true
14 | }
15 | ],
16 | "no-underscore-dangle": "off",
17 | "import/prefer-default-export": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.LayoutsTest do
2 | use StreamClosedCaptionerPhoenixWeb.ConnCase, async: true
3 |
4 | # When testing helpers, you may want to import Phoenix.HTML and
5 | # use functions such as safe_to_string() to convert the helper
6 | # result into an HTML string.
7 | # import Phoenix.HTML
8 | end
9 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/app.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <%= Phoenix.Flash.get(@flash, :info) %>
3 | <%= Phoenix.Flash.get(@flash, :error) %>
4 | <%= @inner_content %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/showcase_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ShowcaseHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("showcase/*")
4 |
5 | def set_stream_thumnail_dimensions(url, width, height) do
6 | url
7 | |> String.replace("{width}", width)
8 | |> String.replace("{height}", height)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/dashboard_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.DashboardHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("dashboard/**/*")
4 |
5 | def display_translation_status(translation_active) when is_nil(translation_active),
6 | do: "Not Activated"
7 |
8 | def display_translation_status(_), do: "Activated"
9 | end
10 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/plugs/auth_error_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.AuthErrorHandler do
2 | import Plug.Conn
3 |
4 | @behaviour Guardian.Plug.ErrorHandler
5 |
6 | @impl Guardian.Plug.ErrorHandler
7 | def auth_error(conn, {type, _reason}, _opts) do
8 | body = Jason.encode!(%{message: to_string(type)})
9 | send_resp(conn, 401, body)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/announcement.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Announcement do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "announcements" do
6 | field :display, :boolean
7 | field :message, :string
8 | end
9 |
10 | @doc false
11 | def changeset(announcement, attrs) do
12 | announcement
13 | |> cast(attrs, [:message, :display])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/extension_info.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.ExtensionInfo do
2 | defstruct [
3 | :active,
4 | :id,
5 | :name,
6 | :version
7 | ]
8 |
9 | @type t :: %__MODULE__{
10 | active: Boolean.t(),
11 | id: String.t(),
12 | name: String.t(),
13 | version: String.t()
14 | }
15 |
16 | use ExConstructor
17 | end
18 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/supporters_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.SupportersController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | @spec index(Plug.Conn.t(), any) :: Plug.Conn.t()
5 | def index(conn, _params) do
6 | {:ok, %{"data" => data}} = GoosemanApp.fetch_supporters()
7 |
8 | render(conn, "index.html", data: data)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/bits_balance_debit/show.html.heex:
--------------------------------------------------------------------------------
1 | Show Bits balance debit
2 |
3 |
4 |
5 | -
6 | User:
7 | <%= @bits_balance_debit.user_id %>
8 |
9 |
10 | -
11 | Amount:
12 | <%= @bits_balance_debit.amount %>
13 |
14 |
15 |
16 |
17 | <%= link "Back", to: ~p"/bits_balance_debits" %>
18 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/extension/captions_payload.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Extension.CaptionsPayload do
2 | @derive Jason.Encoder
3 | @type t :: %__MODULE__{
4 | interim: String.t(),
5 | final: String.t(),
6 | delay: float(),
7 | translations: map()
8 | }
9 | defstruct interim: "", final: "", delay: 0, translations: nil
10 |
11 | use ExConstructor
12 | end
13 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion/page.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion.Page do
2 | @moduledoc """
3 | Simple API wrapper for Notion API
4 | """
5 | import Notion.Base
6 |
7 | @doc """
8 | Notion.Page.retrieve_page("08f40d1e-9a9b-48e7-9da2-0e2362f5e372")
9 | """
10 | def retrieve_page(page_id), do: get("pages/#{page_id}")
11 | defdelegate retrieve_block_children(block_id, params \\ %{}), to: Notion.Block
12 | end
13 |
--------------------------------------------------------------------------------
/assets/js/channels/captions.js:
--------------------------------------------------------------------------------
1 | import socket from '../service/socket';
2 |
3 | // Now that you are connected, you can join channels with a topic:
4 | export const captionsChannel = socket.channel(`captions:${window.userId}`, {});
5 |
6 | captionsChannel
7 | .join()
8 | .receive('ok', (resp) => {
9 | console.debug('Joined successfully');
10 | })
11 | .receive('error', (resp) => {
12 | console.debug('Unable to join');
13 | });
14 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/extension_provider.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.ExtensionProvider do
2 | alias Twitch.Extension.Credentials
3 |
4 | @callback send_pubsub_message_for(
5 | Credentials.t(),
6 | String.t(),
7 | Twitch.Extension.CaptionsPayload.t()
8 | ) ::
9 | {:error, HTTPoison.Error.t()}
10 | | {:ok, HTTPoison.Response.t()}
11 | end
12 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # StreamClosedCaptionerPhoenix.Repo.insert!(%StreamClosedCaptionerPhoenix.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/assets/js/controllers/dropdown_controller.js:
--------------------------------------------------------------------------------
1 | import { ApplicationController } from "stimulus-use"
2 |
3 | export default class extends ApplicationController {
4 | get menuTarget() {
5 | return this.element.querySelector('div[data-target="dropdown.menu"]')
6 | }
7 |
8 | connect() {
9 | this.toggleClass = this.data.get("class") || "hidden"
10 | }
11 |
12 | toggle() {
13 | this.menuTarget.classList.toggle(this.toggleClass)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.configure(timeout: :infinity)
2 | ExUnit.start()
3 | Ecto.Adapters.SQL.Sandbox.mode(StreamClosedCaptionerPhoenix.Repo, :manual)
4 | {:ok, _} = Application.ensure_all_started(:ex_machina)
5 |
6 | # Mocks services out using their provider
7 | Mox.defmock(Azure.MockCognitive, for: Azure.CognitiveProvider)
8 | Mox.defmock(Twitch.MockExtension, for: Twitch.ExtensionProvider)
9 | Mox.defmock(Twitch.MockHelix, for: Twitch.HelixProvider)
10 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211102040347_add_oban_jobs_table.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.AddObanJobsTable do
2 | use Ecto.Migration
3 |
4 | def up do
5 | Oban.Migrations.up()
6 | end
7 |
8 | # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if
9 | # necessary, regardless of which version we've migrated `up` to.
10 | def down do
11 | Oban.Migrations.down(version: 1)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/assets/js/init_toast.js:
--------------------------------------------------------------------------------
1 | export const InitToast = {
2 | mounted() {
3 | init()
4 | }
5 | }
6 |
7 | const init = () => {
8 | const toastEl = document.querySelector('.toast')
9 | if (toastEl && toastEl.innerText !== '') {
10 | toastEl.classList.add("mr-4")
11 | toastEl.classList.remove("hidden")
12 |
13 | setTimeout(() => {
14 | toastEl.classList.remove("mr-4")
15 | toastEl.classList.add("hidden")
16 | }, 3000);
17 | }
18 | }
19 |
20 | init()
21 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/_notification.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
6 | Notifications
7 |
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "sqltools.connections": [
3 | {
4 | "previewLimit": 50,
5 | "server": "localhost",
6 | "port": 5432,
7 | "askForPassword": true,
8 | "driver": "PostgreSQL",
9 | "name": "development",
10 | "database": "stream-cc-development",
11 | "username": "postgres"
12 | }
13 | ],
14 | "elixirLinter.useStrict": true,
15 | "editor.formatOnSave": true,
16 | "cSpell.words": [
17 | "Captioner"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/azure.ex:
--------------------------------------------------------------------------------
1 | defmodule Azure do
2 | use Nebulex.Caching
3 |
4 | def api_client,
5 | do: Application.get_env(:stream_closed_captioner_phoenix, :azure_cognitive_client)
6 |
7 | @spec perform_translations(String.t(), [String.t()], String.t()) ::
8 | Azure.Cognitive.Translations.t()
9 | def perform_translations(from_language, to_languages, text) do
10 | api_client().translate(from_language, to_languages, text)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_confirmation/new.html.heex:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <%= form_for :user, ~p"/users/confirm", fn f -> %>
4 | <%= label f, :email %>
5 | <%= text_input f, :email, required: true %>
6 |
7 |
8 | <%= submit "Resend confirmation instructions" %>
9 |
10 | <% end %>
11 |
12 |
13 | <%= link "Register", to: ~p"/users/register" %> |
14 | <%= link "Log in", to: ~p"/users/log_in" %>
15 |
16 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/guardian.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Guardian do
2 | use Guardian, otp_app: :stream_closed_captioner_phoenix
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 |
6 | def subject_for_token(resource, _claims) do
7 | sub = to_string(resource.id)
8 | {:ok, sub}
9 | end
10 |
11 | def resource_from_claims(claims) do
12 | id = claims["sub"]
13 | resource = Accounts.get_user!(id)
14 | {:ok, resource}
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/announcements_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.AnnouncementsController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | def index(conn, _params) do
5 | pages =
6 | case Notion.Database.query_database("8f9f6076e708455fbb263b3ba9ca48db") do
7 | {:ok, response} -> get_in(response, ["results"])
8 | _ -> []
9 | end
10 |
11 | render(conn, "index.html", pages: pages)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/new.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | <%%= render "form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :create)) %>
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: mix
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | open-pull-requests-limit: 20
8 | target-branch: master
9 | labels:
10 | - "dependencies"
11 | - "mix"
12 | - package-ecosystem: npm
13 | directory: "/assets"
14 | schedule:
15 | interval: "daily"
16 | open-pull-requests-limit: 20
17 | target-branch: master
18 | labels:
19 | - "dependencies"
20 | - "npm"
21 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/live/page_live_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.PageLiveTest do
2 | use StreamClosedCaptionerPhoenixWeb.ConnCase, async: true
3 |
4 | import Phoenix.LiveViewTest
5 |
6 | @tag :skip
7 | test "disconnected and connected render", %{conn: conn} do
8 | {:ok, page_live, disconnected_html} = live(conn, "/")
9 | assert disconnected_html =~ "Boilerplate Generator"
10 | assert render(page_live) =~ "Boilerplate Generator"
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/assets/js/controllers/index.js:
--------------------------------------------------------------------------------
1 | export { default as CaptionsController } from './captions_controller';
2 | export { default as DarkmodeController } from './darkmode_controller'
3 | export { default as DropdownController } from './dropdown_controller';
4 | export { default as ObsController } from './obs_controller';
5 | export { default as TranslationController } from './translations_controller';
6 | export { default as TwitchController } from './twitch_controller';
7 | export { default as ZoomController } from './zoom_controller';
8 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/extension_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.ExtensionChannel do
2 | defstruct [
3 | :broadcaster_id,
4 | :broadcaster_name,
5 | :game_id,
6 | :game_name,
7 | :title
8 | ]
9 |
10 | @type t :: %__MODULE__{
11 | game_name: String.t(),
12 | game_id: String.t(),
13 | title: String.t(),
14 | broadcaster_name: String.t(),
15 | broadcaster_id: String.t()
16 | }
17 |
18 | use ExConstructor
19 | end
20 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | <%%= render "form.html", Map.put(assigns, :action, Routes.<%= schema.route_helper %>_path(@conn, :update, @<%= schema.singular %>)) %>
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20211221041043_create_eventsub_subscriptions.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.CreateEventsubSubscriptions do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:eventsub_subscriptions) do
6 | add :user_id, references(:users, on_delete: :delete_all), null: false
7 | add :type, :string
8 | add :subscription_id, :string
9 |
10 | timestamps()
11 | end
12 |
13 | create unique_index(:eventsub_subscriptions, [:subscription_id])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/azure/cognitive/translation.ex:
--------------------------------------------------------------------------------
1 | defmodule Azure.Cognitive.Translation do
2 | @type t :: %__MODULE__{
3 | text: String.t(),
4 | name: String.t()
5 | }
6 | @derive Jason.Encoder
7 | defstruct [
8 | :text,
9 | :name
10 | ]
11 |
12 | use ExConstructor
13 |
14 | def new(data, args \\ []) do
15 | res = super(data, args)
16 | name = StreamClosedCaptionerPhoenix.Settings.translatable_languages[data["to"]]
17 | %{res | name: name }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/assets/js/service/zoom-sequence.js:
--------------------------------------------------------------------------------
1 | import { isNil } from 'ramda';
2 |
3 | export const setZoomSequence = (url, value = 1) => {
4 | const urlObj = new URL(url);
5 | const id = urlObj.searchParams.get('id');
6 |
7 | localStorage.setItem(`zoom:${id}`, value);
8 | };
9 |
10 | export const getZoomSequence = (url) => {
11 | const urlObj = new URL(url);
12 | const id = urlObj.searchParams.get('id');
13 |
14 | const result = localStorage.getItem(`zoom:${id}`);
15 | if (!isNil(result)) {
16 | return parseInt(localStorage.getItem(`zoom:${id}`), 10);
17 | }
18 |
19 | return 1;
20 | };
21 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/product_data.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.ProductData do
2 | alias Twitch.Helix.Cost
3 |
4 | defstruct [
5 | :sku,
6 | :cost,
7 | :display_name,
8 | :in_development
9 | ]
10 |
11 | @type t :: %__MODULE__{
12 | sku: String.t(),
13 | cost: non_neg_integer(),
14 | display_name: String.t(),
15 | in_development: boolean()
16 | }
17 |
18 | use ExConstructor
19 |
20 | def new(data, args \\ []) do
21 | res = super(data, args)
22 | %{res | cost: Cost.new(res.cost)}
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/eventsub.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.EventSub do
2 | defstruct [
3 | :condition,
4 | :cost,
5 | :created_at,
6 | :id,
7 | :status,
8 | :transport,
9 | :type,
10 | :version
11 | ]
12 |
13 | @type t :: %__MODULE__{
14 | condition: map(),
15 | cost: integer(),
16 | created_at: String.t(),
17 | id: String.t(),
18 | status: String.t(),
19 | transport: map(),
20 | type: String.t(),
21 | version: String.t()
22 | }
23 | use ExConstructor
24 | end
25 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/azure/cognitive/translations.ex:
--------------------------------------------------------------------------------
1 | defmodule Azure.Cognitive.Translations do
2 | alias Azure.Cognitive.Translation
3 |
4 | @type t :: %__MODULE__{
5 | translations: list(Translation.t())
6 | }
7 | @derive Jason.Encoder
8 | defstruct [
9 | :translations
10 | ]
11 |
12 | use ExConstructor
13 |
14 | def new(data, args \\ []) do
15 | res = super(data, args)
16 | %{res | translations: Enum.reduce(res.translations, %{}, fn translation, acc ->
17 | Map.put(acc, translation["to"], Translation.new(translation))
18 | end)}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/dashboard/switch/_switch_off.html.eex:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/priv/static/images/patreon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/showcase_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ShowcaseController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | @spec index(Plug.Conn.t(), any) :: Plug.Conn.t()
5 | def index(conn, _params) do
6 | active_channel_ids = StreamClosedCaptionerPhoenixWeb.UserTracker.recently_active_channels()
7 |
8 | # Fetch information about the channel to display for Twitch API
9 | stream_list = Twitch.get_live_streams(active_channel_ids)
10 |
11 | # Send the data to the front end
12 | render(conn, "index.html", data: stream_list)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance_queries.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalanceQueries do
2 | import Ecto.Query, warn: false
3 |
4 | alias StreamClosedCaptionerPhoenix.Bits.BitsBalance
5 |
6 | def all(query \\ base()), do: query
7 |
8 | def with_user_id(query \\ base(), user_id) do
9 | query
10 | |> where([bits_balance], bits_balance.user_id == ^user_id)
11 | end
12 |
13 | def with_id(query \\ base(), id) do
14 | query
15 | |> where([bits_balance], bits_balance.id == ^id)
16 | end
17 |
18 | defp base do
19 | from(_ in BitsBalance, as: :bits_balance)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/dashboard/switch/_switch_on.html.eex:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/cards.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Components.Cards do
2 | @moduledoc """
3 | Card components
4 | """
5 | use Phoenix.Component
6 |
7 | attr(:shadow, :boolean, default: false)
8 | attr(:border, :boolean, default: false)
9 | slot(:inner_block, required: true)
10 |
11 | def card(assigns) do
12 | ~H"""
13 |
18 | <%= render_slot(@inner_block) %>
19 |
20 | """
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ErrorViewTest do
2 | use StreamClosedCaptionerPhoenixWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | # import Phoenix.View
6 |
7 | # test "renders 404.html" do
8 | # assert render_to_string(StreamClosedCaptionerPhoenixWeb.ErrorView, "404.html", []) ~=
9 | # "Not Found"
10 | # end
11 |
12 | # test "renders 500.html" do
13 | # assert render_to_string(StreamClosedCaptionerPhoenixWeb.ErrorView, "500.html", []) ~=
14 | # "Internal Server Error"
15 | # end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/settings/translate_languages_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Settings.TranslateLanguageAdmin do
2 | alias StreamClosedCaptionerPhoenix.{Accounts, Settings}
3 |
4 | def get_user(%{user_id: id}) do
5 | id
6 | |> Accounts.get_user!()
7 | |> Map.get(:username)
8 | end
9 |
10 | def index(_) do
11 | [
12 | user_id: %{name: "User", value: fn p -> get_user(p) end},
13 | language: nil
14 | ]
15 | end
16 |
17 | def form_fields(_) do
18 | [
19 | user_id: %{update: :readonly},
20 | language: %{choices: Settings.translateable_language_list()}
21 | ]
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20201205194222_create_users_auth_tables.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.CreateUsersAuthTables do
2 | use Ecto.Migration
3 |
4 | def change do
5 | execute "CREATE EXTENSION IF NOT EXISTS citext", ""
6 |
7 | create table(:users_tokens) do
8 | add :user_id, references(:users, on_delete: :delete_all), null: false
9 | add :token, :binary, null: false
10 | add :context, :string, null: false
11 | add :sent_to, :string
12 | timestamps(updated_at: false)
13 | end
14 |
15 | create index(:users_tokens, [:user_id])
16 | create unique_index(:users_tokens, [:context, :token])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/error_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ErrorHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("error/*")
4 |
5 | # If you want to customize a particular status code
6 | # for a certain format, you may uncomment below.
7 | # def render("500.html", _assigns) do
8 | # "Internal Server Error"
9 | # end
10 |
11 | # By default, Phoenix returns the status message from
12 | # the template name. For example, "404.html" becomes
13 | # "Not Found".
14 | def template_not_found(template, _assigns) do
15 | Phoenix.Controller.status_message_from_template(template)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion.Utils do
2 | @moduledoc false
3 |
4 | def api_key, do: Application.get_env(:stream_closed_captioner_phoenix, :api_key)
5 |
6 | def notion_version,
7 | do: Application.get_env(:stream_closed_captioner_phoenix, :notion_version, "2021-05-13")
8 |
9 | def auth_header, do: [{"Authorization", "Bearer " <> api_key()}]
10 | def version_header, do: [{"Notion-Version", notion_version()}]
11 | def content_header, do: [{"Content-Type", "application/json"}]
12 | def request_headers, do: version_header() ++ auth_header()
13 | def post_request_headers, do: version_header() ++ auth_header() ++ content_header()
14 | end
15 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance_debit_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalanceDebitAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 |
4 | def search_fields(_schema) do
5 | [
6 | user: [:email, :username, :uid]
7 | ]
8 | end
9 |
10 | def ordering(_schema) do
11 | [desc: :created_at]
12 | end
13 |
14 | def get_user(%{user_id: id}) do
15 | id
16 | |> Accounts.get_user!()
17 | |> Map.get(:username)
18 | end
19 |
20 | def index(_) do
21 | [
22 | id: nil,
23 | user_id: %{name: "User", value: fn p -> get_user(p) end},
24 | amount: nil,
25 | created_at: nil
26 | ]
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalanceAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 |
4 | def search_fields(_schema) do
5 | [
6 | user: [:email, :username, :uid]
7 | ]
8 | end
9 |
10 | def ordering(_schema) do
11 | [desc: :updated_at]
12 | end
13 |
14 | def get_user(%{user_id: id}) do
15 | id
16 | |> Accounts.get_user!()
17 | |> Map.get(:username)
18 | end
19 |
20 | def index(_) do
21 | [
22 | id: nil,
23 | user_id: %{name: "User", value: fn p -> get_user(p) end},
24 | balance: nil,
25 | created_at: nil,
26 | updated_at: nil
27 | ]
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance_debit.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalanceDebit do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "bits_balance_debits" do
6 | field :amount, :integer
7 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
8 |
9 | timestamps(inserted_at: :created_at)
10 | end
11 |
12 | @doc false
13 | def changeset(bits_balance_debit, attrs) do
14 | bits_balance_debit
15 | |> cast(attrs, [:user_id, :amount])
16 | |> foreign_key_constraint(:user_id, name: "fk_rails_51a42188b3")
17 | |> validate_required([:user_id, :amount])
18 | |> validate_number(:amount, greater_than_or_equal_to: 500)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/live.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <%= Phoenix.Flash.get(@flash, :info) %>
4 |
5 | <%= Phoenix.Flash.get(@flash, :error) %>
7 |
8 | <%= @inner_content %>
9 |
10 |
11 | <%= if assigns[:live_action] in [:new, :edit] do %>
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/resolvers/accounts_oauth.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Resolvers.AccountsOauth do
2 | def get_channel_info(_, %{id: _id}, %{
3 | context: %{decoded_token: decoded_token}
4 | }) do
5 | id = Map.get(decoded_token, "channel_id")
6 |
7 | case StreamClosedCaptionerPhoenix.AccountsOauth.get_user_for_provider("twitch", id) do
8 | nil ->
9 | {:error, "Channel #{id} not found"}
10 |
11 | user ->
12 | {:ok, user}
13 | end
14 | end
15 |
16 | def get_channel_info(_parent, _args, _resolution) do
17 | {:error,
18 | "Access denied, missing or invalid token. Please try again with correct credentials."}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/form_component.html.leex:
--------------------------------------------------------------------------------
1 | <%%= f = form_for @changeset, "#",
2 | id: "<%= schema.singular %>-form",
3 | class: "block",
4 | phx_target: @myself,
5 | phx_change: "validate",
6 | phx_submit: "save" %>
7 | <%= for {label, input, error} <- inputs, input do %><%= if String.match?(input, ~r/checkbox/) do %>
8 |
9 | <%= input %>
10 | <%= label %>
11 | <%= error %>
12 |
13 | <% else %>
14 |
15 | <%= label %>
16 | <%= input %>
17 | <%= error %>
18 |
19 | <% end %><% end %>
20 |
21 | <%%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-primary btn-sm" %>
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/accounts/eventsub_subscription.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Accounts.EventsubSubscription do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "eventsub_subscriptions" do
6 | field :subscription_id, :string
7 | field :type, :string
8 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
9 |
10 | timestamps()
11 | end
12 |
13 | @doc false
14 | def changeset(eventsub_subscription, attrs) do
15 | eventsub_subscription
16 | |> cast(attrs, [:type, :subscription_id, :user_id])
17 | |> foreign_key_constraint(:user_id)
18 | |> unique_constraint(:subscription_id)
19 | |> validate_required([:type, :subscription_id, :user_id])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/bits_balance_debit_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.BitsBalanceDebitController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | alias StreamClosedCaptionerPhoenix.Bits
5 |
6 | def index(conn, _params) do
7 | user = conn.assigns.current_user
8 |
9 | bits_balance_debits = Bits.list_users_bits_balance_debits(user)
10 | render(conn, "index.html", bits_balance_debits: bits_balance_debits)
11 | end
12 |
13 | def show(conn, %{"id" => id}) do
14 | user = conn.assigns.current_user
15 |
16 | bits_balance_debit = Bits.get_users_bits_balance_debit!(user, id)
17 | render(conn, "show.html", bits_balance_debit: bits_balance_debit)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/component_library.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ComponentLibrary do
2 | defmacro __using__(_) do
3 | quote do
4 | import StreamClosedCaptionerPhoenixWeb.ComponentLibrary
5 | # Import additional component modules below
6 | import StreamClosedCaptionerPhoenixWeb.Components.Dropdowns
7 | import StreamClosedCaptionerPhoenixWeb.Components.Cards
8 | import StreamClosedCaptionerPhoenixWeb.Components.Tables
9 | end
10 | end
11 |
12 | @moduledoc """
13 | This module is added and used in StreamClosedCaptionerPhoenixWeb. The idea is
14 | different component modules can be added and imported in the macro section above.
15 | """
16 | use Phoenix.Component
17 | end
18 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/release.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Release do
2 | @moduledoc """
3 | Used for executing DB release tasks when run in production without Mix
4 | installed.
5 | """
6 | @app :stream_closed_captioner_phoenix
7 |
8 | def migrate do
9 | load_app()
10 |
11 | for repo <- repos() do
12 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
13 | end
14 | end
15 |
16 | def rollback(repo, version) do
17 | load_app()
18 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
19 | end
20 |
21 | defp repos do
22 | Application.fetch_env!(@app, :ecto_repos)
23 | end
24 |
25 | defp load_app do
26 | Application.load(@app)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/resolvers/settings.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Resolvers.Settings do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 | alias StreamClosedCaptionerPhoenix.Settings
4 |
5 | def get_translations_info(%Accounts.User{} = user, _, _resolution) do
6 | debit = StreamClosedCaptionerPhoenix.Bits.get_user_active_debit(user.id)
7 | time = Map.get(debit || %{}, :created_at)
8 |
9 | {:ok,
10 | %{
11 | languages: Settings.get_formatted_translate_languages_by_user(user.id),
12 | activated: !is_nil(debit),
13 | created_at: format_datetime(time)
14 | }}
15 | end
16 |
17 | defp format_datetime(time) when is_nil(time), do: nil
18 | defp format_datetime(time), do: Timex.to_datetime(time, "Etc/UTC")
19 | end
20 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/transcripts/message.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Transcripts.Message do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "messages" do
6 | field :text, :string
7 | belongs_to :transcript, StreamClosedCaptionerPhoenix.Transcripts.Transcript
8 |
9 | timestamps(inserted_at: :created_at)
10 | end
11 |
12 | @doc false
13 | def changeset(message, attrs) do
14 | message
15 | |> cast(attrs, [:transcript_id, :text])
16 | |> foreign_key_constraint(:transcript_id, name: "fk_rails_832df11d70")
17 | |> validate_required([:transcript_id, :text])
18 | end
19 |
20 | @doc false
21 | def update_changeset(message, attrs) do
22 | message
23 | |> cast(attrs, [:text])
24 | |> validate_required([:text])
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Show do
2 | use <%= inspect context.web_module %>, :live_view
3 |
4 | alias <%= inspect context.module %>
5 |
6 | @impl true
7 | def mount(_params, _session, socket) do
8 | {:ok, socket}
9 | end
10 |
11 | @impl true
12 | def handle_params(%{"id" => id}, _, socket) do
13 | {:noreply,
14 | socket
15 | |> assign(:page_title, page_title(socket.assigns.live_action))
16 | |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))}
17 | end
18 |
19 | defp page_title(:show), do: "Show <%= schema.human_singular %>"
20 | defp page_title(:edit), do: "Edit <%= schema.human_singular %>"
21 | end
22 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_transaction_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsTransactionAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 |
4 | def search_fields(_schema) do
5 | [
6 | user: [:username, :purchaser_uid, :sku, :transaction_id]
7 | ]
8 | end
9 |
10 | def ordering(_schema) do
11 | [desc: :id]
12 | end
13 |
14 | def get_user(%{user_id: id}) do
15 | id
16 | |> Accounts.get_user!()
17 | |> Map.get(:username)
18 | end
19 |
20 | def index(_) do
21 | [
22 | id: nil,
23 | user_id: %{name: "User", value: fn p -> get_user(p) end},
24 | amount: nil,
25 | display_name: nil,
26 | purchaser_uid: nil,
27 | sku: nil,
28 | time: nil,
29 | transaction_id: nil
30 | ]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20220914052253_swap_primary_oban_indexes.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.SwapPrimaryObanIndexes do
2 | use Ecto.Migration
3 | @disable_ddl_transaction true
4 | @disable_migration_lock true
5 |
6 | def change do
7 | create_if_not_exists index(
8 | :oban_jobs,
9 | [:state, :queue, :priority, :scheduled_at, :id],
10 | concurrently: true,
11 | prefix: "public"
12 | )
13 |
14 | drop_if_exists index(
15 | :oban_jobs,
16 | [:queue, :state, :priority, :scheduled_at, :id],
17 | concurrently: true,
18 | prefix: "public"
19 | )
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/supporters_html.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.SupportersHTML do
2 | use StreamClosedCaptionerPhoenixWeb, :html
3 | embed_templates("supporters/*")
4 |
5 | @spec filter_twitch_subscribers(list()) :: list()
6 | def filter_twitch_subscribers(subscribes) do
7 | Enum.filter(subscribes, fn x -> x["user"]["displayName"] != "Talk2meGooseman" end)
8 | end
9 |
10 | @spec filter_patreon_subscribers(list()) :: list()
11 | def filter_patreon_subscribers(subscribes) do
12 | Enum.filter(subscribes, fn x ->
13 | x["campaignLifetimeSupportCents"] > 0
14 | end)
15 | end
16 |
17 | def get_polite_status(support_value) do
18 | case support_value > 0 do
19 | true -> "Active Patron"
20 | _ -> "Former Patron"
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "mix_task",
9 | "name": "mix (Default task)",
10 | "request": "launch",
11 | "projectDir": "${workspaceRoot}"
12 | },
13 | {
14 | "type": "mix_task",
15 | "name": "mix test",
16 | "request": "launch",
17 | "task": "test",
18 | "taskArgs": [
19 | "--trace"
20 | ],
21 | "startApps": true,
22 | "projectDir": "${workspaceRoot}",
23 | "requireFiles": [
24 | "test/**/test_helper.exs",
25 | "test/**/*_test.exs"
26 | ]
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :stream_closed_captioner_phoenix, StreamClosedCaptionerPhoenixWeb.Endpoint,
13 | cache_static_manifest: "priv/static/cache_manifest.json"
14 |
15 | # Do not print debug messages in production
16 | config :logger, level: :info
17 |
18 | config :stream_closed_captioner_phoenix, StreamClosedCaptionerPhoenixWeb.Endpoint,
19 | force_ssl: [rewrite_on: [:x_forwarded_proto]]
20 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/gooseman_app.ex:
--------------------------------------------------------------------------------
1 | defmodule GoosemanApp do
2 | def fetch_supporters() do
3 | Neuron.Config.set(url: "https://guzman.codes/api")
4 |
5 | query = """
6 | {
7 | twitch {
8 | broadcasterSubscriptions(broadcasterId: "120750024") {
9 | user {
10 | id
11 | profileImageUrl
12 | displayName
13 | description
14 | }
15 | tier
16 | }
17 | }
18 | patreon {
19 | campaignMembers {
20 | fullName
21 | currentlyEntitledAmountCents
22 | campaignLifetimeSupportCents
23 | }
24 | }
25 | }
26 | """
27 |
28 | case Neuron.query(query) do
29 | {:ok, %{body: body}} -> {:ok, body}
30 | _ -> {:error, "Request failed"}
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/user.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.User do
2 | defstruct [
3 | :id,
4 | :login,
5 | :display_name,
6 | :type,
7 | :broadcaster_type,
8 | :description,
9 | :profile_image_url,
10 | :offline_image_url,
11 | :view_count,
12 | :email,
13 | :created_at
14 | ]
15 |
16 | @type t :: %__MODULE__{
17 | id: String.t(),
18 | login: String.t(),
19 | display_name: String.t(),
20 | type: String.t(),
21 | broadcaster_type: String.t(),
22 | description: String.t(),
23 | profile_image_url: String.t(),
24 | offline_image_url: String.t(),
25 | view_count: non_neg_integer(),
26 | email: String.t(),
27 | created_at: String.t()
28 | }
29 | use ExConstructor
30 | end
31 |
--------------------------------------------------------------------------------
/priv/static/cache_manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "!comment!":"This file was auto-generated by `mix phx.digest`. Remove it and all generated artefacts with `mix phx.digest.clean --all`",
3 | "version":1,
4 | "latest":{"assets/app.css":"assets/app-3fb118a31ac92da99365f49ea6978665.css","assets/app.js":"assets/app-60ac421655d21e45b16dbeb37f7098a6.js"},
5 | "digests":{"assets/app-3fb118a31ac92da99365f49ea6978665.css":{"digest":"3fb118a31ac92da99365f49ea6978665","logical_path":"assets/app.css","mtime":63846600355,"sha512":"555IGhA8APydGLp5AqRiaoclwgGf41YwcBeKGomvw06nq5jpiXrIEpX5JgXGiE/tyTqKKPrVrH5i7Q+PixxS0w==","size":381131},"assets/app-60ac421655d21e45b16dbeb37f7098a6.js":{"digest":"60ac421655d21e45b16dbeb37f7098a6","logical_path":"assets/app.js","mtime":63846600355,"sha512":"yTVxPkDC7lVT1/o/ex39PGQz9D+Zz6TFvfm6ZcmwNjXXegib+S2u5sI9EHcGHj9CEuPgkyXVot7sfexWXJh4+w==","size":257521}}
6 | }
7 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/form.html.eex:
--------------------------------------------------------------------------------
1 | <%%= form_for @changeset, @action, fn f -> %>
2 | <%%= if @changeset.action do %>
3 |
4 |
Oops, something went wrong! Please check the errors below.
5 |
6 | <%% end %>
7 | <%= for {label, input, error} <- inputs, input do %><%= if String.match?(input, ~r/checkbox/) do %>
8 |
9 | <%= input %>
10 | <%= label %>
11 | <%= error %>
12 |
13 | <% else %>
14 |
15 | <%= label %>
16 | <%= input %>
17 | <%= error %>
18 |
19 | <% end %><% end %>
20 |
21 | <%%= submit "Save", class: "btn btn-primary btn-sm" %>
22 | <%%= link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index), class: "ml-2 btn btn-link btn-sm" %>
23 |
24 | <%% end %>
25 |
--------------------------------------------------------------------------------
/assets/js/stimulus.js:
--------------------------------------------------------------------------------
1 | import { Application } from '@hotwired/stimulus';
2 | import { CaptionsController } from './controllers';
3 | import { DropdownController } from './controllers';
4 | import { DarkmodeController } from './controllers';
5 | import { ObsController } from './controllers';
6 | import { TranslationController } from './controllers';
7 | import { TwitchController } from './controllers';
8 | import { ZoomController } from './controllers';
9 |
10 | window.Stimulus = Application.start();
11 |
12 | Stimulus.register('captions', CaptionsController);
13 | Stimulus.register('darkmode', DarkmodeController);
14 | Stimulus.register('dropdown', DropdownController);
15 | Stimulus.register('obs', ObsController);
16 | Stimulus.register('translations', TranslationController);
17 | Stimulus.register('twitch', TwitchController);
18 | Stimulus.register('zoom', ZoomController);
19 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/stream.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.Stream do
2 | defstruct [
3 | :game_id,
4 | :game_name,
5 | :id,
6 | :language,
7 | :started_at,
8 | :thumbnail_url,
9 | :title,
10 | :type,
11 | :user_id,
12 | :user_login,
13 | :user_name,
14 | :viewer_count
15 | ]
16 |
17 | @type t :: %__MODULE__{
18 | game_id: String.t(),
19 | game_name: String.t(),
20 | id: String.t(),
21 | language: String.t(),
22 | started_at: String.t(),
23 | thumbnail_url: String.t(),
24 | title: String.t(),
25 | type: String.t(),
26 | user_id: String.t(),
27 | user_login: String.t(),
28 | user_name: String.t(),
29 | viewer_count: String.t()
30 | }
31 |
32 | use ExConstructor
33 | end
34 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/resolvers/accounts.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Resolvers.Accounts do
2 | def get_user(_parent, %{id: id}, _resolution) do
3 | case StreamClosedCaptionerPhoenix.Accounts.get_user!(id) do
4 | nil ->
5 | {:error, "User ID #{id} not found"}
6 |
7 | user ->
8 | {:ok, user}
9 | end
10 | end
11 |
12 | def get_me(_parent, _params, %{
13 | context: %{current_user: current_user}
14 | })
15 | when current_user != nil do
16 | case StreamClosedCaptionerPhoenix.Accounts.get_user!(current_user.id) do
17 | nil ->
18 | {:error, "Access Denied, no current user set"}
19 |
20 | user ->
21 | {:ok, user}
22 | end
23 | end
24 |
25 | def get_me(_parent, _args, _resolution) do
26 | {:error, "Access denied, missing current user"}
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/support/fixtures/accounts_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.AccountsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `StreamClosedCaptionerPhoenix.Accounts` context.
5 | """
6 |
7 | def unique_user_email, do: "user#{System.unique_integer()}@example.com"
8 | def valid_user_password, do: "hello world!"
9 |
10 | def user_fixture(attrs \\ %{}) do
11 | {:ok, %{user: user}} =
12 | attrs
13 | |> Enum.into(%{
14 | email: unique_user_email(),
15 | password: valid_user_password()
16 | })
17 | |> StreamClosedCaptionerPhoenix.Accounts.register_user()
18 |
19 | user
20 | end
21 |
22 | def extract_user_token(fun) do
23 | captured = fun.(&"[TOKEN]#{&1}[TOKEN]")
24 | [_, token, _] = String.split(captured.text_body, "[TOKEN]")
25 | token
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalance do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "bits_balances" do
6 | field :balance, :integer, default: 0
7 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
8 |
9 | timestamps(inserted_at: :created_at)
10 | end
11 |
12 | @doc false
13 | def changeset(bits_balance, attrs) do
14 | bits_balance
15 | |> cast(attrs, [:user_id, :balance])
16 | |> foreign_key_constraint(:user_id, name: "fk_rails_1a2fa97ecf")
17 | |> unique_constraint(:user_id, name: "index_bits_balances_on_user_id")
18 | |> validate_required([:user_id])
19 | end
20 |
21 | @doc false
22 | def update_changeset(bits_balance, attrs) do
23 | bits_balance
24 | |> cast(attrs, [:balance])
25 | |> validate_required([:balance])
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/dropdowns.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Components.Dropdowns do
2 | @moduledoc """
3 | Dropdown components
4 | """
5 | use Phoenix.Component
6 |
7 | alias Phoenix.LiveView.JS
8 |
9 | def toggle_dropdown(id, js \\ %JS{}) do
10 | js
11 | |> JS.toggle(
12 | to: id,
13 | in:
14 | {"transition ease-out duration-150", "opacity-0 translate-y-1",
15 | "opacity-100 translate-y-0"},
16 | out:
17 | {"transition ease-in duration-100", "opacity-100 translate-y-0",
18 | "opacity-0 translate-y-1"}
19 | )
20 | end
21 |
22 | def close_dropdown(id, js \\ %JS{}) do
23 | js
24 | |> JS.hide(
25 | to: id,
26 | transition:
27 | {"transition ease-in duration-100", "opacity-100 translate-y-0",
28 | "opacity-0 translate-y-1"}
29 | )
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import StreamClosedCaptionerPhoenixWeb.Gettext
9 |
10 | # Simple translation
11 | gettext("Here is the string to translate")
12 |
13 | # Plural translation
14 | ngettext("Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3)
17 |
18 | # Domain-based translation
19 | dgettext("errors", "Here is the error message to translate")
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :stream_closed_captioner_phoenix
24 | end
25 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/transcript.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tag() %>
8 | <.live_title suffix=" · Closed Captions">
9 | <%= assigns[:page_title] || "Stream Closed Captioner" %>
10 |
11 | <%= render_tags_all(assigns[:meta_tags] || %{}) %>
12 |
13 |
14 |
16 |
17 |
18 |
19 | <%= @inner_content %>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/schema/middleware/authorized_introspection.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Schema.Middleware.AuthorizedIntrospection do
2 | @moduledoc """
3 | Disable or restrict schema introspection to authorized requests
4 | """
5 | @behaviour Absinthe.Plugin
6 |
7 | @impl Absinthe.Plugin
8 | def before_resolution(exec) do
9 | if Enum.find(exec.result.emitter.selections, fn %{name: field_name} ->
10 | field_name == "__schema" && Mix.env() != :dev
11 | end) do
12 | %{
13 | exec
14 | | validation_errors: [
15 | %Absinthe.Phase.Error{message: "Unauthorized", phase: __MODULE__}
16 | ]
17 | }
18 | else
19 | exec
20 | end
21 | end
22 |
23 | @impl Absinthe.Plugin
24 | def after_resolution(exec), do: exec
25 |
26 | @impl Absinthe.Plugin
27 | def pipeline(pipeline, _exec), do: pipeline
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/bits_balance_debit/index.html.heex:
--------------------------------------------------------------------------------
1 | Listing Bits balance debits
2 |
3 |
4 |
5 |
6 | | User |
7 | Amount |
8 |
9 | |
10 |
11 |
12 |
13 | <%= for bits_balance_debit <- @bits_balance_debits do %>
14 |
15 | | <%= bits_balance_debit.user_id %> |
16 | <%= bits_balance_debit.amount %> |
17 |
18 |
19 | <%= link "Show", to: ~p"/bits_balance_debits/#{bits_balance_debit}" %>
20 | <%= link "Edit", to: Routes.bits_balance_debit_path(@conn, :edit, bits_balance_debit) %>
21 | <%= link "Delete", to: Routes.bits_balance_debit_path(@conn, :delete, bits_balance_debit), method: :delete, data: [confirm: "Are you sure?"] %>
22 | |
23 |
24 | <% end %>
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/transcripts/transcript.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Transcripts.Transcript do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "transcripts" do
6 | field :name, :string
7 | field :session, :string
8 |
9 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
10 | has_many :messages, StreamClosedCaptionerPhoenix.Transcripts.Message
11 |
12 | timestamps(inserted_at: :created_at)
13 | end
14 |
15 | @doc false
16 | def changeset(transcript, attrs) do
17 | transcript
18 | |> cast(attrs, [:user_id, :name, :session])
19 | |> foreign_key_constraint(:user_id, name: "fk_rails_d177bec369")
20 | |> validate_required([:user_id, :name, :session])
21 | end
22 |
23 | @doc false
24 | def update_changeset(transcript, attrs) do
25 | transcript
26 | |> cast(attrs, [:name])
27 | |> validate_required([:name])
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | <%= for {k, _} <- schema.attrs do %>
11 |
12 | <%= Phoenix.Naming.humanize(Atom.to_string(k)) %>:
13 | <%%= @<%= schema.singular %>.<%= k %> %>
14 |
15 | <% end %>
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/accounts/eventsub_subscription_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Accounts.EventsubSubscriptionAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 |
4 | def search_fields(_schema) do
5 | [
6 | user: [:email, :username, :uid]
7 | ]
8 | end
9 |
10 | def widgets(_schema, _conn) do
11 | [
12 | %{
13 | type: "tidbit",
14 | title: "Active EventSub Subscriptions",
15 | content: Twitch.get_event_subscriptions("") |> Enum.count(),
16 | order: 1,
17 | width: 3,
18 | icon: ''
19 | }
20 | ]
21 | end
22 |
23 | def get_user(%{user_id: id}) do
24 | id
25 | |> Accounts.get_user!()
26 | |> Map.get(:username)
27 | end
28 |
29 | def index(_) do
30 | [
31 | user_id: %{name: "User", value: fn p -> get_user(p) end},
32 | subscription_id: nil,
33 | type: nil
34 | ]
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/channels/captions_channel_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.CaptionsChannelTest do
2 | use StreamClosedCaptionerPhoenixWeb.ChannelCase, async: true
3 |
4 | import StreamClosedCaptionerPhoenix.Factory
5 |
6 | setup do
7 | [stream_settings, _] = insert_pair(:stream_settings, user: fn -> build(:user) end)
8 |
9 | {:ok, _, socket} =
10 | StreamClosedCaptionerPhoenixWeb.UserSocket
11 | |> socket("user_id", %{current_user: stream_settings.user})
12 | |> subscribe_and_join(
13 | StreamClosedCaptionerPhoenixWeb.CaptionsChannel,
14 | "captions:#{stream_settings.user.id}"
15 | )
16 |
17 | %{socket: socket}
18 | end
19 |
20 | # test "ping replies with status ok", %{socket: socket} do
21 | # ref =
22 | # push(socket, "publish", %{"interim" => "hello", "final" => "goodbye", "session" => "123"})
23 |
24 | # assert_reply ref, :ok, %{"hello" => "there"}
25 | # end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion.Parser do
2 | @moduledoc """
3 | Generic parser to parse api response
4 | """
5 |
6 | @type status_code :: integer
7 | @type headers :: map
8 | @type response ::
9 | {:ok, struct}
10 | | {:error, map, status_code}
11 | | any
12 |
13 | @doc """
14 | Parses the response from API calls
15 | """
16 | @spec parse(tuple) :: response
17 | def parse({:ok, %HTTPoison.Response{body: body, headers: _, status_code: status}})
18 | when status in [200, 201],
19 | do: {:ok, parse_response_body(body)}
20 |
21 | def parse({:error, %HTTPoison.Error{id: _, reason: reason}}), do: {:error, %{reason: reason}}
22 |
23 | def parse({:ok, %HTTPoison.Response{body: body, headers: _, status_code: status}}),
24 | do: {:error, parse_response_body(body), status}
25 |
26 | def parse(response), do: response
27 |
28 | defp parse_response_body(body), do: Poison.decode!(body)
29 | end
30 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Parser do
2 | @moduledoc """
3 | Generic parser to parse api response
4 | """
5 |
6 | @type status_code :: integer
7 | @type headers :: map
8 | @type response ::
9 | {:ok, struct}
10 | | {:error, map, status_code}
11 | | any
12 |
13 | @doc """
14 | Parses the response from API calls
15 | """
16 | @spec parse(tuple) :: response
17 | def parse({:ok, %HTTPoison.Response{body: body, headers: _, status_code: status}})
18 | when status in [200, 201],
19 | do: {:ok, parse_response_body(body)}
20 |
21 | def parse({:error, %HTTPoison.Error{id: _, reason: reason}}), do: {:error, %{reason: reason}}
22 |
23 | def parse({:ok, %HTTPoison.Response{body: body, headers: _, status_code: status}}),
24 | do: {:error, parse_response_body(body), status}
25 |
26 | def parse(response), do: response
27 |
28 | defp parse_response_body(body), do: Poison.decode!(body)
29 | end
30 |
--------------------------------------------------------------------------------
/test/support/fixtures/transcripts_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.TranscriptsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `StreamClosedCaptionerPhoenix.Transcripts` context.
5 | """
6 |
7 | import StreamClosedCaptionerPhoenix.AccountsFixtures
8 |
9 | def transcript_fixture(attrs \\ %{}) do
10 | {:ok, transcript} =
11 | attrs
12 | |> Enum.into(%{
13 | name: "some name",
14 | session: "some session",
15 | user_id: user_fixture().id
16 | })
17 | |> StreamClosedCaptionerPhoenix.Transcripts.create_transcript()
18 |
19 | transcript
20 | end
21 |
22 | def message_fixture(attrs \\ %{}) do
23 | {:ok, message} =
24 | attrs
25 | |> Enum.into(%{
26 | text: "some text",
27 | transcript_id: transcript_fixture().id
28 | })
29 | |> StreamClosedCaptionerPhoenix.Transcripts.create_message()
30 |
31 | message
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/notion/base.ex:
--------------------------------------------------------------------------------
1 | defmodule Notion.Base do
2 | @moduledoc false
3 | alias NewRelic.Instrumented.HTTPoison
4 |
5 | alias Notion.Parser
6 | import Notion.Utils
7 |
8 | @base_url "https://api.notion.com/v1"
9 |
10 | def get(path_arg, query_params \\ %{}) do
11 | path_arg
12 | |> build_url(query_params)
13 | |> HTTPoison.get(request_headers())
14 | |> Parser.parse()
15 | end
16 |
17 | def post(path_arg, body \\ %{}) do
18 | json_body = Jason.encode!(body)
19 |
20 | path_arg
21 | |> build_url()
22 | |> HTTPoison.post(json_body, post_request_headers())
23 | |> Parser.parse()
24 | end
25 |
26 | defp build_url(path_arg, query_params \\ %{}) do
27 | query_params = process_params(query_params)
28 |
29 | "#{@base_url}/#{path_arg}?#{URI.encode_query(query_params)}"
30 | end
31 |
32 | def process_params(params) do
33 | %{}
34 | |> Map.merge(params)
35 | |> Map.delete(:__struct__)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix/transaction.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Helix.Transaction do
2 | alias Twitch.Helix.ProductData
3 |
4 | defstruct [
5 | :id,
6 | :timestamp,
7 | :broadcaster_id,
8 | :broadcaster_login,
9 | :broadcaster_name,
10 | :user_id,
11 | :user_login,
12 | :user_name,
13 | :product_type,
14 | :product_data
15 | ]
16 |
17 | @type t :: %__MODULE__{
18 | id: String.t(),
19 | timestamp: String.t(),
20 | broadcaster_id: String.t(),
21 | broadcaster_login: String.t(),
22 | broadcaster_name: String.t(),
23 | user_id: String.t(),
24 | user_login: String.t(),
25 | user_name: String.t(),
26 | product_type: String.t(),
27 | product_data: String.t()
28 | }
29 | use ExConstructor
30 |
31 | def new(data, args \\ []) do
32 | res = super(data, args)
33 | %{res | product_data: ProductData.new(res.product_data)}
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/resolvers/bits.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Resolvers.Bits do
2 | alias StreamClosedCaptionerPhoenix.{Accounts, Bits}
3 |
4 | def bits_balance(%Accounts.User{} = user, _args, _resolution) do
5 | bits_balance = Bits.get_bits_balance_for_user(user)
6 |
7 | case bits_balance do
8 | nil ->
9 | {:error, "Bits balance not found"}
10 |
11 | bits_balance ->
12 | {:ok, bits_balance}
13 | end
14 | end
15 |
16 | def process_bits_transaction(_parent, %{channel_id: channel_id}, %{
17 | context: %{decoded_token: decoded_token}
18 | }) do
19 | case Bits.process_bits_transaction(channel_id, decoded_token) do
20 | {:ok, _} -> {:ok, %{message: "Transaction Successful"}}
21 | {:error, _, message, _} -> {:error, %{message: message}}
22 | end
23 | end
24 |
25 | def process_bits_transaction(_parent, _args, _resolution) do
26 | {:error, "Access denied, missing or invalid token"}
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/plugs/maintenance.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Maintenance do
2 | @behaviour Plug
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 |
6 | # MyAppWeb.Maintenance.begin() and end it via MyAppWeb.Maintenance.finish().
7 |
8 | def begin, do: :persistent_term.put(__MODULE__, true)
9 | def finish, do: :persistent_term.erase(__MODULE__)
10 |
11 | @impl true
12 | def init(_), do: []
13 |
14 | @impl true
15 | def call(conn, _opts) do
16 | user = conn.assigns[:current_user]
17 |
18 | if Accounts.is_admin?(user) do
19 | conn
20 | else
21 | case :persistent_term.get(__MODULE__, false) do
22 | false ->
23 | conn
24 |
25 | true ->
26 | conn
27 | |> Plug.Conn.send_resp(
28 | 503,
29 | "Sorry! The site is currently under maintenance, please come back in a little bit."
30 | )
31 | |> Plug.Conn.halt()
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/schema/accounts_types.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Schema.AccountsTypes do
2 | use Absinthe.Schema.Notation
3 |
4 | alias StreamClosedCaptionerPhoenixWeb.Resolvers
5 |
6 | @desc "A user"
7 | object :user do
8 | field :uid, :id
9 | field :username, :string
10 | end
11 |
12 | @desc "Get information about a user"
13 | object :me do
14 | field :id, :id
15 |
16 | field :extension_installed, :boolean do
17 | resolve(&has_extension_installed?/3)
18 | end
19 | end
20 |
21 | object :accounts_queries do
22 | @desc "Fetch the current users information"
23 | field :me, :me do
24 | resolve(&Resolvers.Accounts.get_me/3)
25 | end
26 | end
27 |
28 | # Return a boolean indicating whether the user has the extension installed
29 | def has_extension_installed?(user, _args, _resolution) do
30 | status = StreamClosedCaptionerPhoenix.Accounts.user_has_extension_installed?(user)
31 | {:ok, status}
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20210227232918_create_feature_flags_table.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Repo.Migrations.CreateFeatureFlagsTable do
2 | use Ecto.Migration
3 |
4 | # This migration assumes the default table name of "fun_with_flags_toggles"
5 | # is being used. If you have overriden that via configuration, you should
6 | # change this migration accordingly.
7 |
8 | def up do
9 | create table(:fun_with_flags_toggles, primary_key: false) do
10 | add :id, :bigserial, primary_key: true
11 | add :flag_name, :string, null: false
12 | add :gate_type, :string, null: false
13 | add :target, :string, null: false
14 | add :enabled, :boolean, null: false
15 | end
16 |
17 | create index(
18 | :fun_with_flags_toggles,
19 | [:flag_name, :gate_type, :target],
20 | unique: true,
21 | name: "fwf_flag_name_gate_target_idx"
22 | )
23 | end
24 |
25 | def down do
26 | drop table(:fun_with_flags_toggles)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/jobs/send_chat_reminder.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Jobs.SendChatReminder do
2 | use Oban.Worker, queue: :default
3 |
4 | # %{broadcaster_user_id: "talk2megooseman", broadcaster_user_login: "talk2megooseman"} |> StreamClosedCaptionerPhoenix.Jobs.SendChatReminder.new(schedule_in: 10) |> Oban.insert()
5 |
6 | @impl Oban.Worker
7 | def perform(%Oban.Job{
8 | args: %{
9 | "broadcaster_user_id" => broadcaster_user_id,
10 | "broadcaster_user_login" => broadcaster_user_login
11 | },
12 | errors: errors
13 | }) do
14 | if Enum.any?(errors) do
15 | :cancel
16 | else
17 | if !StreamClosedCaptionerPhoenixWeb.UserTracker.channel_active?(broadcaster_user_id) do
18 | Twitch.send_extension_chat_message(
19 | broadcaster_user_id,
20 | "Hey @#{broadcaster_user_login}, here is your friendly reminder to turn on Stream Closed Captioner."
21 | )
22 | end
23 |
24 | :ok
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/dashboard_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.DashboardController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 | alias StreamClosedCaptionerPhoenix.Bits
4 | alias StreamClosedCaptionerPhoenix.Repo
5 | alias StreamClosedCaptionerPhoenix.Settings
6 |
7 | def index(conn, _params) do
8 | current_user =
9 | conn.assigns.current_user
10 | |> Repo.preload([:stream_settings, :bits_balance])
11 |
12 | twitch_enabled = current_user.provider === "twitch" && is_binary(current_user.uid)
13 |
14 | render(conn, "index.html",
15 | twitch_enabled: twitch_enabled,
16 | stream_settings: current_user.stream_settings,
17 | translation_active: Bits.get_user_active_debit(current_user.id),
18 | bits_balance: current_user.bits_balance.balance,
19 | translate_languages: Settings.get_formatted_translate_languages_by_user(current_user.id),
20 | announcement: StreamClosedCaptionerPhoenix.Announcement |> Repo.first()
21 | )
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | elixir:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | args:
9 | # Elixir Version: 1.9, 1.10, 1.10.4, ...
10 | VARIANT: "1.16"
11 | # Phoenix Version: 1.4.17, 1.5.4, ...
12 | PHOENIX_VERSION: "1.7.7"
13 | # Node Version: 10, 11, ...
14 | INSTALL_NODE: "true"
15 | NODE_VERSION: "18.15.0"
16 | volumes:
17 | - ..:/workspace:cached
18 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
19 | network_mode: service:db
20 |
21 | # Overrides default command so things don't shut down after the process ends.
22 | command: sleep infinity
23 |
24 | db:
25 | image: postgres:latest
26 | restart: unless-stopped
27 | volumes:
28 | - postgres-data:/var/lib/postgresql/data
29 | environment:
30 | POSTGRES_USER: postgres
31 | POSTGRES_PASSWORD: postgres
32 | POSTGRES_DB: app
33 |
34 | volumes:
35 | postgres-data:
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_transaction.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsTransaction do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "bits_transactions" do
6 | field :amount, :integer
7 | field :display_name, :string
8 | field :purchaser_uid, :string
9 | field :sku, :string
10 | field :time, :naive_datetime
11 | field :transaction_id, :string
12 |
13 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
14 | end
15 |
16 | @doc false
17 | def changeset(bits_transaction, attrs) do
18 | bits_transaction
19 | |> cast(attrs, [
20 | :transaction_id,
21 | :user_id,
22 | :time,
23 | :purchaser_uid,
24 | :sku,
25 | :amount,
26 | :display_name
27 | ])
28 | |> unique_constraint(:transaction_id, name: "index_bits_transactions_on_transaction_id")
29 | |> validate_required([
30 | :transaction_id,
31 | :user_id,
32 | :time,
33 | :purchaser_uid,
34 | :sku,
35 | :amount
36 | ])
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/emails.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Emails do
2 | import Bamboo.Email
3 | use Bamboo.Phoenix, view: StreamClosedCaptionerPhoenixWeb.EmailView
4 |
5 | @from "test@example.com"
6 |
7 | def welcome_email(%{email: email}) do
8 | base_email()
9 | |> subject("Welcome!")
10 | |> to(email)
11 | |> render("welcome.html",
12 | title: "Thank you for signing up",
13 | preheader: "Thank you for signing up to the app."
14 | )
15 | |> premail()
16 | end
17 |
18 | defp base_email do
19 | new_email()
20 | |> from(@from)
21 | # Set default layout
22 | |> put_html_layout({StreamClosedCaptionerPhoenixWeb.Layouts, "email.html"})
23 | # Set default text layout
24 | |> put_text_layout({StreamClosedCaptionerPhoenixWeb.Layouts, "email.text"})
25 | end
26 |
27 | defp premail(email) do
28 | html = Premailex.to_inline_css(email.html_body)
29 | text = Premailex.to_text(email.html_body)
30 |
31 | email
32 | |> html_body(html)
33 | |> text_body(text)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/accounts/eventsub_subscription_queries.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Accounts.EventsubSubscriptionQueries do
2 | import Ecto.Query, warn: false
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts.EventsubSubscription
5 |
6 | def all(query \\ base()), do: query
7 |
8 | def with_user_id(query \\ base(), user_id) do
9 | query
10 | |> where([eventsub_subscription], eventsub_subscription.user_id == ^user_id)
11 | end
12 |
13 | def with_id(query \\ base(), id) do
14 | query
15 | |> where([eventsub_subscription], eventsub_subscription.id == ^id)
16 | end
17 |
18 | def with_subscription_id(query \\ base(), subscription_id) do
19 | query
20 | |> where([eventsub_subscription], eventsub_subscription.subscription_id == ^subscription_id)
21 | end
22 |
23 | def with_type(query \\ base(), type) do
24 | query
25 | |> where([eventsub_subscription], eventsub_subscription.type == ^type)
26 | end
27 |
28 | defp base do
29 | from(_ in EventsubSubscription, as: :eventsub_subscription)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/zoom.ex:
--------------------------------------------------------------------------------
1 | defmodule Zoom do
2 | alias NewRelic.Instrumented.HTTPoison
3 |
4 | @spec send_captions_to(String.t() | URI.t(), String.t(), Zoom.Params.t()) ::
5 | {:error, HTTPoison.Error.t()}
6 | | {:ok,
7 | %{
8 | :__struct__ =>
9 | HTTPoison.AsyncResponse | HTTPoison.MaybeRedirect | HTTPoison.Response,
10 | optional(:body) => any,
11 | optional(:headers) => list,
12 | optional(:id) => reference,
13 | optional(:redirect_url) => any,
14 | optional(:request) => HTTPoison.Request.t(),
15 | optional(:request_url) => any,
16 | optional(:status_code) => integer
17 | }}
18 | def send_captions_to(url, text, %Zoom.Params{seq: seq, lang: lang}) do
19 | headers = [
20 | {"Accept", "*/*"},
21 | {"Content-Type", "text/plain"}
22 | ]
23 |
24 | (url <> "&" <> URI.encode_query(%{seq: seq, lang: lang}))
25 | |> HTTPoison.post(text, headers)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_reset_password/new.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Forgot your password?
4 | <%= form_for :user, ~p"/users/reset_password", fn f -> %>
5 |
6 | <%= label f, :email, class: "form-label" %>
7 | <%= text_input f, :email, required: true, class: "form-input" %>
8 |
9 |
10 |
11 | <%= submit "Send password reset instructions", class: "btn btn-dark w-full" %>
12 |
13 | <% end %>
14 |
15 |
16 | I do rememebr my password
17 |
18 |
19 |
20 | <%= link "Register", to: ~p"/users/register", class: "btn btn-link btn-sm dark:text-blue-500" %>
21 | <%= link "Log in", to: ~p"/users/log_in", class: "btn btn-link btn-sm dark:text-blue-500" %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StreamClosedCaptionerPhoenix
2 |
3 | To start your Phoenix server:
4 |
5 | - Install dependencies with `mix deps.get`
6 | - Create and migrate your database with `mix ecto.setup`
7 | - Install Node.js dependencies with `npm install` inside the `assets` directory
8 | - Start Phoenix endpoint with `mix phx.server`
9 |
10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
11 |
12 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
13 |
14 | ## Env configs
15 |
16 | Done through service
17 |
18 | ## Debug Live View
19 |
20 | `liveSocket.enableDebug()`
21 |
22 | ## Migrate database
23 |
24 | `bin/stream_closed_captioner_phoenix eval "StreamClosedCaptionerPhoenix.Release.migrate"`
25 |
26 | ## Debugging on Elastic Beanstalk
27 |
28 | `eb logs`
29 | `eb ssh` to get into the EC2 machine
30 | `sudo -s` on the EC2 machine to run Docker commands and attach to the instance
31 | `docker ps` will list the running containers
32 | `docker exec -i -t container_name COMMAND` will connect you to the container in a Bash shell
33 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/http_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.HttpHelpers do
2 | def auth_request_headers(token),
3 | do: content_header() ++ auth_header(token) ++ client_id_header()
4 |
5 | def content_header, do: [{"Content-Type", "application/json"}]
6 | def client_id, do: System.get_env("TWITCH_CLIENT_ID") || ""
7 | def client_id_header, do: [{"Client-Id", client_id()}]
8 | def extension_version, do: System.get_env("EXTENSION_VERSION") || "1.6.4"
9 |
10 | def webhook_transport,
11 | do: %{
12 | method: "webhook",
13 | callback:
14 | Application.get_env(:stream_closed_captioner_phoenix, :eventsub_callback_url) <>
15 | "/webhooks",
16 | secret: eventsub_secret()
17 | }
18 |
19 | def eventsub_secret, do: System.get_env("TWITCH_EVENTSUB_SECRET") || ""
20 |
21 | def client_secret,
22 | do: Application.get_env(:stream_closed_captioner_phoenix, :twitch_client_secret) || ""
23 |
24 | def token_secret, do: System.get_env("TWITCH_TOKEN_SECRET") || ""
25 | defp auth_header(token), do: [{"Authorization", "Bearer " <> token}]
26 | end
27 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/plugs/context.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Context do
2 | @behaviour Plug
3 |
4 | import Plug.Conn
5 |
6 | def init(opts), do: opts
7 |
8 | def call(conn, _) do
9 | context =
10 | %{}
11 | |> build_user_context(conn)
12 | |> build_token_context(conn)
13 |
14 | put_private(conn, :absinthe, %{context: context})
15 | end
16 |
17 | defp build_user_context(context, conn) do
18 | with "" <> token <- StreamClosedCaptionerPhoenixWeb.UserAuth.fetch_cookie_user_token(conn),
19 | user <-
20 | StreamClosedCaptionerPhoenix.Accounts.get_user_by_session_token(token) do
21 | Map.merge(context, %{current_user: user})
22 | else
23 | _ -> context
24 | end
25 | end
26 |
27 | defp build_token_context(context, conn) do
28 | with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
29 | {:ok, decoded_token} <- Twitch.Jwt.verify_and_validate(token) do
30 | Map.merge(context, %{decoded_token: decoded_token})
31 | else
32 | _ -> context
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/live_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.LiveHelpers do
2 | import Phoenix.LiveView.Helpers
3 |
4 | @doc """
5 | Renders a component inside the `<%= inspect context.web_module %>.ModalComponent` component.
6 |
7 | The rendered modal receives a `:return_to` option to properly update
8 | the URL when the modal is closed.
9 |
10 | ## Examples
11 |
12 | <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent,
13 | id: @<%= schema.singular %>.id || :new,
14 | action: @live_action,
15 | <%= schema.singular %>: @<%= schema.singular %>,
16 | return_to: Routes.<%= schema.singular %>_index_path(@socket, :index) %>
17 | """
18 | def live_modal(socket, component, opts) do
19 | path = Keyword.fetch!(opts, :return_to)
20 | title = Keyword.fetch!(opts, :title)
21 | modal_opts = [id: :modal, return_to: path, title: title, component: component, opts: opts]
22 | live_component(socket, <%= inspect context.web_module %>.ModalComponent, modal_opts)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/support/fixtures/bits_fixture.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.BitsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `StreamClosedCaptionerPhoenix.Bits` context.
5 | """
6 |
7 | import StreamClosedCaptionerPhoenix.AccountsFixtures
8 |
9 | def bits_balance_fixture(attrs \\ %{}) do
10 | {:ok, bits_balance} =
11 | attrs
12 | |> Enum.into(%{
13 | balance: 0,
14 | user_id: user_fixture().id
15 | })
16 | |> StreamClosedCaptionerPhoenix.Bits.create_bits_balance()
17 |
18 | bits_balance
19 | end
20 |
21 | def bits_transaction_fixture(attrs \\ %{}) do
22 | {:ok, bits_transaction} =
23 | attrs
24 | |> Enum.into(%{
25 | amount: 42,
26 | display_name: "some display_name",
27 | purchaser_uid: "some purchaser_uid",
28 | sku: "some sku",
29 | time: ~N[2010-04-17 14:00:00],
30 | transaction_id: "some transaction_id",
31 | user_id: user_fixture().id
32 | })
33 | |> StreamClosedCaptionerPhoenix.Bits.create_bits_transaction()
34 |
35 | bits_transaction
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/types/deepgram.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Types.DeepgramAlternative do
2 | defstruct confidence: 0,
3 | transcript: nil
4 |
5 | use ExConstructor
6 | end
7 |
8 | defmodule StreamClosedCaptionerPhoenix.Types.DeepgramChannel do
9 | alias StreamClosedCaptionerPhoenix.Types.DeepgramAlternative
10 | defstruct alternatives: []
11 |
12 | use ExConstructor
13 |
14 | def new(data, args \\ []) do
15 | res = super(data, args)
16 |
17 | %{
18 | res
19 | | alternatives:
20 | Enum.map(
21 | res.alternatives,
22 | &DeepgramAlternative.new/1
23 | )
24 | }
25 | end
26 | end
27 |
28 | defmodule StreamClosedCaptionerPhoenix.DeepgramResponse do
29 | alias StreamClosedCaptionerPhoenix.Types.DeepgramChannel
30 |
31 | defstruct duration: 0,
32 | start: nil,
33 | is_final: nil,
34 | speech_final: nil,
35 | channel: %{}
36 |
37 | use ExConstructor
38 |
39 | def new(data, args \\ []) do
40 | res = super(data, args)
41 |
42 | %{res | channel: DeepgramChannel.new(res.channel)}
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_registration/_twitch_connect_button.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
26 | Continue with Twitch
27 |
28 |
--------------------------------------------------------------------------------
/assets/js/controllers/darkmode_controller.js:
--------------------------------------------------------------------------------
1 | import { ApplicationController } from "stimulus-use"
2 |
3 | export default class extends ApplicationController {
4 | static targets = ["darkmodeOff", "darkmodeOn"]
5 |
6 | connect() {
7 | if (this.isDarkmode()) {
8 | this.darkmodeIconOn()
9 | } else {
10 | this.darkmodeIconOff()
11 | }
12 | }
13 |
14 | darkmodeIconOff() {
15 | this.darkmodeOnTarget.classList.remove('hidden')
16 | this.darkmodeOffTarget.classList.add('hidden')
17 | }
18 |
19 | darkmodeIconOn() {
20 | this.darkmodeOnTarget.classList.add('hidden')
21 | this.darkmodeOffTarget.classList.remove('hidden')
22 | }
23 |
24 | toggle() {
25 | if (this.isDarkmode()) {
26 | this.darkmodeIconOff()
27 | document.documentElement.classList.remove('dark')
28 | localStorage.theme = 'light'
29 | } else {
30 | this.darkmodeIconOn()
31 | document.documentElement.classList.add('dark')
32 | localStorage.theme = 'dark'
33 | }
34 | }
35 |
36 | isDarkmode() {
37 | return localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/assets/js/service/deepgram.js:
--------------------------------------------------------------------------------
1 | import { curry, isNil } from 'ramda';
2 |
3 | const audio = {
4 | mediaRecorder: null
5 | }
6 |
7 | const constraints = { audio: true };
8 |
9 | const onSuccess = function onSuccess(callback, stream) {
10 | audio.mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
11 |
12 | if (isNil(audio.mediaRecorder)) throw new Error('MediaRecorder is not initialized');
13 |
14 | audio.mediaRecorder.start(1000);
15 |
16 | audio.mediaRecorder.ondataavailable = function onData(evt) {
17 | if (evt.data.size > 0) {
18 | callback(evt.data);
19 | }
20 | };
21 | };
22 |
23 | const curriedOnSuccess = curry(onSuccess);
24 |
25 | const onError = function onError() { };
26 |
27 | export const startDeepgram = (callback) => {
28 | if (navigator.mediaDevices.getUserMedia) {
29 | navigator.mediaDevices.getUserMedia(constraints)
30 | .then(
31 | curriedOnSuccess(callback),
32 | onError,
33 | );
34 | }
35 | };
36 |
37 | export const stopDeepgram = () => {
38 | audio.mediaRecorder.stop();
39 | delete audio.mediaRecorder;
40 | };
41 |
42 | export const isDeepgramActive = () => audio.mediaRecorder?.state === 'recording';
43 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/controllers/bits_balance_debit_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.BitsBalanceDebitControllerTest do
2 | import StreamClosedCaptionerPhoenix.Factory
3 | use StreamClosedCaptionerPhoenixWeb.ConnCase, async: true
4 |
5 | setup :register_and_log_in_user
6 |
7 | describe "index" do
8 | test "lists all bits_balance_debits", %{conn: conn} do
9 | conn = get(conn, Routes.bits_balance_debit_path(conn, :index))
10 | assert html_response(conn, 200) =~ "Listing Bits balance debits"
11 | end
12 | end
13 |
14 | describe "show bits_balance_debit" do
15 | setup [:create_bits_balance_debit]
16 |
17 | test "redirects to show when data is valid", %{
18 | conn: conn,
19 | bits_balance_debit: bits_balance_debit
20 | } do
21 | conn = get(conn, Routes.bits_balance_debit_path(conn, :show, bits_balance_debit.id))
22 | assert html_response(conn, 200) =~ "Show Bits balance debit"
23 | end
24 | end
25 |
26 | defp create_bits_balance_debit(%{user: user}) do
27 | bits_balance_debit = insert(:bits_balance_debit, user: user)
28 | %{bits_balance_debit: bits_balance_debit}
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/support/fixtures/settings_fixture.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.SettingsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `StreamClosedCaptionerPhoenix.Settings` context.
5 | """
6 |
7 | import StreamClosedCaptionerPhoenix.AccountsFixtures
8 |
9 | def stream_settings_fixture(attrs \\ %{}) do
10 | {:ok, settings} =
11 | attrs
12 | |> Enum.into(%{
13 | caption_delay: 42,
14 | cc_box_size: true,
15 | filter_profanity: true,
16 | hide_text_on_load: true,
17 | language: "some language",
18 | pirate_mode: true,
19 | showcase: true,
20 | switch_settings_position: true,
21 | text_uppercase: true,
22 | user_id: user_fixture().id
23 | })
24 | |> StreamClosedCaptionerPhoenix.Settings.create_stream_settings()
25 |
26 | settings
27 | end
28 |
29 | def translate_language_fixture(attrs \\ %{}) do
30 | {:ok, translate_language} =
31 | attrs
32 | |> Enum.into(%{language: "en", user_id: user_fixture().id})
33 | |> StreamClosedCaptionerPhoenix.Settings.create_translate_language()
34 |
35 | translate_language
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/stream_settings_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.StreamSettingsController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | alias StreamClosedCaptionerPhoenix.Settings
5 |
6 | def edit(conn, _params) do
7 | user = conn.assigns.current_user
8 | stream_settings = Settings.get_stream_settings_by_user_id!(user.id)
9 |
10 | changeset = Settings.change_stream_settings(stream_settings)
11 | render(conn, "edit.html", stream_settings: stream_settings, changeset: changeset)
12 | end
13 |
14 | def update(conn, %{"stream_settings" => stream_settings_params}) do
15 | user = conn.assigns.current_user
16 | stream_settings = Settings.get_stream_settings_by_user_id!(user.id)
17 |
18 | case Settings.update_stream_settings(stream_settings, stream_settings_params) do
19 | {:ok, _} ->
20 | conn
21 | |> put_flash(:info, "Stream settings updated successfully.")
22 | |> redirect(to: Routes.caption_settings_index_path(conn, :index))
23 |
24 | {:error, %Ecto.Changeset{} = changeset} ->
25 | render(conn, "edit.html", stream_settings: stream_settings, changeset: changeset)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_balance_debit_queries.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsBalanceDebitQueries do
2 | import Ecto.Query, warn: false
3 |
4 | @seconds_in_hours 3600
5 |
6 | alias StreamClosedCaptionerPhoenix.Bits.BitsBalanceDebit
7 |
8 | def all(query \\ base()), do: query
9 |
10 | def with_user_id(query \\ base(), user_id) do
11 | query
12 | |> where([bits_balance_debit], bits_balance_debit.user_id == ^user_id)
13 | end
14 |
15 | def with_id(query \\ base(), id) do
16 | query
17 | |> where([bits_balance_debit], bits_balance_debit.id == ^id)
18 | end
19 |
20 | def less_than_one_day_ago(query \\ base()) do
21 | one_day_ago = NaiveDateTime.utc_now() |> NaiveDateTime.add(@seconds_in_hours * -24)
22 | # set seconds to 0
23 | one_day_ago =
24 | NaiveDateTime.new!(
25 | one_day_ago.year,
26 | one_day_ago.month,
27 | one_day_ago.day,
28 | one_day_ago.hour,
29 | one_day_ago.minute,
30 | 0
31 | )
32 |
33 | query
34 | |> where([bits_balance_debit], bits_balance_debit.created_at >= ^one_day_ago)
35 | end
36 |
37 | defp base do
38 | from(_ in BitsBalanceDebit, as: :bits_balance_debit)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tag() %>
8 | <.live_title suffix=" · Closed Captions">
9 | <%= assigns[:page_title] || "Stream Closed Captioner" %>
10 |
11 | <%= render_tags_all(assigns[:meta_tags] || %{}) %>
12 |
13 |
14 |
16 |
17 |
18 |
19 | <%= _header(assigns) %>
20 | <%= @inner_content %>
21 |
22 | <%= _footer(assigns) %>
23 |
25 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/live/live_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.LiveHelpers do
2 | import Phoenix.LiveView.Helpers
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 |
6 | @doc """
7 | Renders a component inside the `StreamClosedCaptionerPhoenixWeb.ModalComponent` component.
8 |
9 | The rendered modal receives a `:return_to` option to properly update
10 | the URL when the modal is closed.
11 |
12 | ## Examples
13 |
14 | <%= live_modal @socket, StreamClosedCaptionerPhoenixWeb.CaptionSettingsLive.FormComponent,
15 | id: @stream_settings.id || :new,
16 | action: @live_action,
17 | stream_settings: @stream_settings,
18 | return_to: Routes.stream_settings_index_path(@socket, :index) %>
19 | """
20 | def live_modal(component, opts) do
21 | path = Keyword.fetch!(opts, :return_to)
22 | title = Keyword.fetch!(opts, :title)
23 | modal_opts = [id: :modal, return_to: path, title: title, component: component, opts: opts]
24 | live_component(StreamClosedCaptionerPhoenixWeb.ModalComponent, modal_opts)
25 | end
26 |
27 | def session_current_user(session) do
28 | session
29 | |> Map.get("user_token")
30 | |> Accounts.get_user_by_session_token()
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/schema/types/custom/json_type.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Schema.Types.Custom.JSON 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 | """
6 | use Absinthe.Schema.Notation
7 |
8 | scalar :json, name: "Json" do
9 | description("""
10 | The `Json` scalar type represents arbitrary json string data, represented as UTF-8
11 | character sequences. The Json type is most often used to represent a free-form
12 | human-readable json string.
13 | """)
14 |
15 | serialize(&encode/1)
16 | parse(&decode/1)
17 | end
18 |
19 | @spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error
20 | @spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
21 | defp decode(%Absinthe.Blueprint.Input.String{value: value}) do
22 | case Jason.decode(value) do
23 | {:ok, result} -> {:ok, result}
24 | _ -> :error
25 | end
26 | end
27 |
28 | defp decode(%Absinthe.Blueprint.Input.Null{}) do
29 | {:ok, nil}
30 | end
31 |
32 | defp decode(_) do
33 | :error
34 | end
35 |
36 | defp encode(value), do: value
37 | end
38 |
--------------------------------------------------------------------------------
/assets/js/utils/browser_compatibility.js:
--------------------------------------------------------------------------------
1 | function isChromium() {
2 | if (navigator.userAgentData.brands) {
3 | const hasGoogleChromeBrand = navigator.userAgentData.brands.find(
4 | (b) => b.brand === 'Google Chrome'
5 | );
6 |
7 | if (hasGoogleChromeBrand) {
8 | return true;
9 | }
10 | } else {
11 | // Possibly older version of Chrome or Chromium that does not have
12 | // navigator.userAgentData.brands
13 |
14 | for (
15 | let i = 0, u = 'Chromium', l = u.length;
16 | i < navigator.plugins.length;
17 | i++
18 | ) {
19 | if (
20 | navigator.plugins[i].name != null &&
21 | navigator.plugins[i].name.substr(0, l) === u
22 | ) {
23 | return true;
24 | }
25 | }
26 |
27 | return false;
28 | }
29 | }
30 |
31 | const isEdge = () => {
32 | return navigator.userAgent && /(Edg\/|Edge)/.test(navigator.userAgent);
33 | };
34 |
35 | const isChromeiOS = () => {
36 | return navigator.userAgent && navigator.userAgent.match('CriOS');
37 | };
38 |
39 | export const isBrowserCompatible = () => {
40 | return (!('webkitSpeechRecognition' in window) ||
41 | navigator.userAgent.indexOf('Opera') !== -1 ||
42 | isChromium() ||
43 | isEdge() ||
44 | isChromeiOS()) &&
45 | !window.Cypress
46 | };
47 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/schema/types/custom/datetime_type.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.Schema.Types.Custom.DateTime do
2 | use Absinthe.Schema.Notation
3 |
4 | scalar :datetime, name: "DateTime" do
5 | description("""
6 | The `DateTime` scalar type represents a date and time in the UTC
7 | timezone. The DateTime appears in a JSON response as an ISO8601 formatted
8 | string, including UTC timezone ("Z"). The parsed date and time string will
9 | be converted to UTC and any UTC offset other than 0 will be rejected..
10 | """)
11 |
12 | serialize(&encode/1)
13 | parse(&parse_datetime/1)
14 | end
15 |
16 | @spec parse_datetime(Absinthe.Blueprint.Input.String.t()) :: {:ok, DateTime.t()} | :error
17 | @spec parse_datetime(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
18 | defp parse_datetime(%Absinthe.Blueprint.Input.String{value: value}) do
19 | case DateTime.from_iso8601(value) do
20 | {:ok, datetime, 0} -> {:ok, datetime}
21 | {:ok, _datetime, _offset} -> :error
22 | _error -> :error
23 | end
24 | end
25 |
26 | defp parse_datetime(%Absinthe.Blueprint.Input.Null{}) do
27 | {:ok, nil}
28 | end
29 |
30 | defp parse_datetime(_) do
31 | :error
32 | end
33 |
34 | defp encode(value), do: DateTime.to_iso8601(value)
35 | end
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/extension.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Extension do
2 | import Helpers
3 |
4 | alias NewRelic.Instrumented.HTTPoison
5 | alias Twitch.ExtensionProvider
6 | alias Twitch.Extension.Credentials
7 | @behaviour ExtensionProvider
8 |
9 | @broadcaster :broadcaster
10 | @spec broadcaster_segment :: :broadcaster
11 | def broadcaster_segment, do: @broadcaster
12 |
13 | @global :global
14 | @spec global_segment :: :global
15 | def global_segment, do: @global
16 |
17 | @developer :developer
18 | @spec developer_segment :: :developer
19 | def developer_segment, do: @developer
20 |
21 | @impl ExtensionProvider
22 | def send_pubsub_message_for(
23 | %Credentials{} = %{client_id: client_id, jwt_token: token},
24 | channel_id,
25 | message
26 | ) do
27 | headers = [
28 | {"Content-Type", "application/json"},
29 | {"Client-Id", client_id},
30 | {"Authorization", "Bearer #{token}"}
31 | ]
32 |
33 | body =
34 | Jason.encode!(%{
35 | message: Jason.encode!(message),
36 | content_type: "application/json",
37 | targets: ["broadcast"]
38 | })
39 |
40 | encode_url_and_params("https://api.twitch.tv/extensions/message/" <> channel_id)
41 | |> HTTPoison.post(body, headers)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/gql_config.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.GqlConfig do
2 | def configuration do
3 | [
4 | schema: StreamClosedCaptionerPhoenixWeb.Schema,
5 | pipeline: {__MODULE__, :absinthe_pipeline},
6 | analyze_complexity: true,
7 | max_complexity: 50
8 | ]
9 | end
10 |
11 | def absinthe_pipeline(config, options) do
12 | options = Absinthe.Pipeline.options(options)
13 |
14 | config
15 | |> Absinthe.Plug.default_pipeline(options)
16 | |> Absinthe.Pipeline.insert_after(
17 | Absinthe.Phase.Document.Complexity.Result,
18 | {AbsintheSecurity.Phase.IntrospectionCheck, options}
19 | )
20 | |> Absinthe.Pipeline.insert_after(
21 | Absinthe.Phase.Document.Result,
22 | {AbsintheSecurity.Phase.FieldSuggestionsCheck, options}
23 | )
24 | |> Absinthe.Pipeline.insert_after(
25 | Absinthe.Phase.Document.Complexity.Result,
26 | {AbsintheSecurity.Phase.MaxAliasesCheck, options}
27 | )
28 | |> Absinthe.Pipeline.insert_after(
29 | Absinthe.Phase.Document.Complexity.Result,
30 | {AbsintheSecurity.Phase.MaxDepthCheck, options}
31 | )
32 | |> Absinthe.Pipeline.insert_after(
33 | Absinthe.Phase.Document.Complexity.Result,
34 | {AbsintheSecurity.Phase.MaxDirectivesCheck, options}
35 | )
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/live/page_live.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.PageLive do
2 | use StreamClosedCaptionerPhoenixWeb, :live_view
3 |
4 | @impl true
5 | def mount(_params, _session, socket) do
6 | {:ok, assign(socket, query: "", results: %{})}
7 | end
8 |
9 | @impl true
10 | def handle_event("suggest", %{"q" => query}, socket) do
11 | {:noreply, assign(socket, results: search(query), query: query)}
12 | end
13 |
14 | @impl true
15 | def handle_event("search", %{"q" => query}, socket) do
16 | case search(query) do
17 | %{^query => vsn} ->
18 | {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
19 |
20 | _ ->
21 | {:noreply,
22 | socket
23 | |> put_flash(:error, "No dependencies found matching \"#{query}\"")
24 | |> assign(results: %{}, query: query)}
25 | end
26 | end
27 |
28 | defp search(query) do
29 | if not StreamClosedCaptionerPhoenixWeb.Endpoint.config(:code_reloader) do
30 | raise "action disabled when not in development"
31 | end
32 |
33 | for {app, desc, vsn} <- Application.started_applications(),
34 | app = to_string(app),
35 | String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
36 | into: %{},
37 | do: {app, vsn}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/transcripts/transcript_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Transcripts.TranscriptAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 |
4 | def ordering(_schema) do
5 | [desc: :id]
6 | end
7 |
8 | def get_user(%{user_id: id}) do
9 | id
10 | |> Accounts.get_user!()
11 | |> Map.get(:username)
12 | end
13 |
14 | def index(_) do
15 | [
16 | user_id: %{name: "User", value: fn p -> get_user(p) end},
17 | name: nil,
18 | session: nil
19 | ]
20 | end
21 |
22 | # def form_fields(_) do
23 | # [
24 | # user_id: %{update: :readonly},
25 | # caption_delay: nil,
26 | # cc_box_size: nil,
27 | # filter_profanity: nil,
28 | # hide_text_on_load: nil,
29 | # language: nil,
30 | # pirate_mode: nil,
31 | # showcase: nil,
32 | # switch_settings_position: nil,
33 | # text_uppercase: nil,
34 | # ]
35 | # end
36 |
37 | def scheduled_tasks(_) do
38 | [
39 | %{
40 | name: "Remove Old Transcripts",
41 | initial_value: nil,
42 | every: 15,
43 | action: fn _ ->
44 | # count = Bakery.Products.cache_product_count()
45 | # "count" will be passed to this function in its next run.
46 | {:ok, nil}
47 | end
48 | }
49 | ]
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use StreamClosedCaptionerPhoenixWeb.ChannelCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with channels
23 | import Phoenix.ChannelTest
24 | import StreamClosedCaptionerPhoenixWeb.ChannelCase
25 |
26 | # The default endpoint for testing
27 | @endpoint StreamClosedCaptionerPhoenixWeb.Endpoint
28 | end
29 | end
30 |
31 | setup tags do
32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(StreamClosedCaptionerPhoenix.Repo)
33 |
34 | unless tags[:async] do
35 | Ecto.Adapters.SQL.Sandbox.mode(StreamClosedCaptionerPhoenix.Repo, {:shared, self()})
36 | end
37 |
38 | :ok
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/modal_component.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.ModalComponent do
2 | use <%= inspect context.web_module %>, :live_component
3 |
4 | @impl true
5 | def render(assigns) do
6 | ~L"""
7 |
13 |
14 |
15 |
16 |
<%%= @title %>
17 | <%%= live_patch raw("×"), to: @return_to, class: "text-gray-500 hover:text-gray-600 font-bold text-lg" %>
18 |
19 |
20 | <%%= live_component @socket, @component, @opts %>
21 |
22 |
23 |
24 |
25 | """
26 | end
27 |
28 | @impl true
29 | def handle_event("close", _, socket) do
30 | {:noreply, push_patch(socket, to: socket.assigns.return_to)}
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/show.html.leex:
--------------------------------------------------------------------------------
1 | <%%= if @live_action in [:edit] do %>
2 | <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent,
3 | id: @<%= schema.singular %>.id,
4 | title: @page_title,
5 | action: @live_action,
6 | <%= schema.singular %>: @<%= schema.singular %>,
7 | return_to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, @<%= schema.singular %>) %>
8 | <%% end %>
9 |
10 |
11 |
12 |
13 |
18 |
19 | <%= for {k, _} <- schema.attrs do %>
20 |
21 | <%= Phoenix.Naming.humanize(Atom.to_string(k)) %>:
22 | <%%= @<%= schema.singular %>.<%= k %> %>
23 |
24 | <% end %>
25 |
26 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # This file excludes paths from the Docker build context.
2 | #
3 | # By default, Docker's build context includes all files (and folders) in the
4 | # current directory. Even if a file isn't copied into the container it is still sent to
5 | # the Docker daemon.
6 | #
7 | # There are multiple reasons to exclude files from the build context:
8 | #
9 | # 1. Prevent nested folders from being copied into the container (ex: exclude
10 | # /assets/node_modules when copying /assets)
11 | # 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
12 | # 3. Avoid sending files containing sensitive information
13 | #
14 | # More information on using .dockerignore is available here:
15 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file
16 |
17 | .dockerignore
18 |
19 | # Ignore git, but keep git HEAD and refs to access current commit hash if needed:
20 | #
21 | # $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
22 | # d0b8727759e1e0e7aa3d41707d12376e373d5ecc
23 | .git
24 | !.git/HEAD
25 | !.git/refs
26 |
27 | # Common development/test artifacts
28 | /cover/
29 | /doc/
30 | /test/
31 | /tmp/
32 | .elixir_ls
33 |
34 | # Mix artifacts
35 | /_build/
36 | /deps/
37 | *.ez
38 |
39 | # Generated on crash by the VM
40 | erl_crash.dump
41 |
42 | # Static artifacts - These should be fetched and built inside the Docker image
43 | /assets/node_modules/
44 | /priv/static/assets/
45 | /priv/static/cache_manifest.json
46 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_registration_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserRegistrationController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 | alias StreamClosedCaptionerPhoenix.Accounts.User
6 | alias StreamClosedCaptionerPhoenixWeb.UserAuth
7 |
8 | def new(conn, _params) do
9 | changeset = Accounts.change_user_registration(%User{})
10 | render(conn, "new.html", changeset: changeset)
11 | end
12 |
13 | @spec create(Plug.Conn.t(), map) :: Plug.Conn.t()
14 | def create(conn, %{"user" => user_params}) do
15 | case Accounts.register_user(user_params) do
16 | {:ok, %{user: user}} ->
17 | conn
18 | |> put_flash(:info, "User created successfully.")
19 | |> UserAuth.log_in_user(user)
20 |
21 | {:error, _, %Ecto.Changeset{} = changeset, _} ->
22 | render(conn, "new.html", changeset: changeset)
23 | end
24 | end
25 |
26 | def delete(conn, _params) do
27 | current_user = conn.assigns.current_user
28 |
29 | case Accounts.delete_user(current_user) do
30 | {:ok, _} ->
31 | conn
32 | |> put_flash(:info, "Account successfully deleted.")
33 | |> redirect(to: "/")
34 |
35 | {:error, reason} ->
36 | conn
37 | |> put_flash(:error, reason)
38 | |> redirect(to: Routes.user_settings_path(conn, :edit))
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/assets/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors');
2 | const plugin = require('tailwindcss/plugin');
3 |
4 | module.exports = {
5 | darkMode: 'class',
6 | content: {
7 | content: ['./js/**/*.js',
8 | '../lib/*_web.ex',
9 | '../lib/*_web/**/*.*ex'
10 | ],
11 | options: {
12 | safelist: ['dark'],
13 | }
14 | },
15 | plugins: [
16 | require('nightwind'),
17 | require('@tailwindcss/aspect-ratio'),
18 | require('kutty'),
19 | require('@tailwindcss/typography'),
20 | require('@tailwindcss/forms'),
21 | plugin(({ addVariant }) => addVariant('phx-no-feedback', ['.phx-no-feedback&', '.phx-no-feedback &'])),
22 | plugin(({ addVariant }) => addVariant('phx-click-loading', ['.phx-click-loading&', '.phx-click-loading &'])),
23 | plugin(({ addVariant }) => addVariant('phx-submit-loading', ['.phx-submit-loading&', '.phx-submit-loading &'])),
24 | plugin(({ addVariant }) => addVariant('phx-change-loading', ['.phx-change-loading&', '.phx-change-loading &'])),
25 | ],
26 | theme: {
27 | extend: {
28 | colors: {
29 | green: colors.emerald,
30 | yellow: colors.amber,
31 | purple: colors.violet,
32 | }
33 | },
34 | nightwind: {
35 | colors: {
36 | white: 'gray.800',
37 | red: {
38 | 100: 'red.100',
39 | },
40 | yellow: {
41 | 100: 'yellow.100',
42 | },
43 | },
44 | },
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/error/500.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tag() %>
8 | <.live_title suffix=" · Closed Captions">
9 | <%= assigns[:page_title] || "Stream Closed Captioner" %>
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
500
25 |
26 | Sorry, the servers have crashed.
27 |
28 |
29 | If this problem continues messag me on Twitter
30 | @Talk2meGooseman
31 | or go back to the homepage.
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/live/modal_component.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ModalComponent do
2 | use StreamClosedCaptionerPhoenixWeb, :live_component
3 |
4 | @impl true
5 | def render(assigns) do
6 | ~H"""
7 |
18 |
19 |
20 |
21 |
<%= @title %>
22 | <.link navigate={@return_to} class="text-gray-500 hover:text-gray-600 font-bold text-lg">
23 | <%= raw("×") %>
24 |
25 |
26 |
27 | <.live_component module={@component} opts={@opts} />
28 |
29 |
30 |
31 |
32 | """
33 | end
34 |
35 | @impl true
36 | def handle_event("close", _, socket) do
37 | {:noreply, push_patch(socket, to: socket.assigns.return_to)}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/session.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tag() %>
8 | <.live_title suffix=" · Closed Captions">
9 | <%= assigns[:page_title] || "Stream Closed Captioner" %>
10 |
11 | <%= render_tags_all(assigns[:meta_tags] || %{}) %>
12 |
13 |
14 |
16 |
17 |
18 |
19 | <%= _header(assigns) %>
20 |
21 |
22 |
23 | <%= Phoenix.Flash.get(@flash, :info) %>
24 |
25 |
26 | <%= Phoenix.Flash.get(@flash, :error) %>
27 |
28 |
29 | <%= @inner_content %>
30 |
31 |
32 | <%= _footer(assigns) %>
33 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/live/caption_settings_live/form_component.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.CaptionSettingsLive.FormComponent do
2 | use StreamClosedCaptionerPhoenixWeb, :live_component
3 |
4 | alias StreamClosedCaptionerPhoenix.Settings
5 |
6 | @impl true
7 | def update(%{stream_settings: stream_settings} = assigns, socket) do
8 | changeset = Settings.change_stream_settings(stream_settings)
9 | language_selection = Settings.spoken_languages()
10 |
11 | {:ok,
12 | socket
13 | |> assign(assigns)
14 | |> assign(:changeset, changeset)
15 | |> assign(:language_selection, language_selection)}
16 | end
17 |
18 | @impl true
19 | def handle_event("validate", %{"stream_settings" => stream_settings_params}, socket) do
20 | changeset =
21 | socket.assigns.stream_settings
22 | |> Settings.change_stream_settings(stream_settings_params)
23 | |> Map.put(:action, :validate)
24 |
25 | {:noreply, assign(socket, :changeset, changeset)}
26 | end
27 |
28 | def handle_event("save", %{"stream_settings" => stream_settings_params}, socket) do
29 | case Settings.update_stream_settings(socket.assigns.stream_settings, stream_settings_params) do
30 | {:ok, _stream_settings} ->
31 | {:noreply,
32 | socket
33 | |> put_flash(:info, "Stream settings updated successfully")}
34 |
35 | {:error, %Ecto.Changeset{} = changeset} ->
36 | {:noreply, assign(socket, :changeset, changeset)}
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/announcements/index.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
Everything Stream Closed Captioner, where you can find out about news, updates, and
6 | features.
7 |
8 |
9 | <%= for page <- @pages do %>
10 |
26 | <% end %>
27 |
28 |
29 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_reset_password/edit.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Reset Password
4 | <%= form_for @changeset, ~p"/users/reset_password/#{@token}", fn f -> %>
5 | <%= if @changeset.action do %>
6 |
7 | Oops, something went wrong! Please check the errors below.
8 |
9 | <% end %>
10 |
11 |
12 | <%= label f, :password, "New password", class: "form-label" %>
13 | <%= password_input f, :password, required: true, class: "form-input" %>
14 | <%= error_tag f, :password %>
15 |
16 |
17 |
18 | <%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
19 | <%= password_input f, :password_confirmation, required: true, class: "form-input" %>
20 | <%= error_tag f, :password_confirmation %>
21 |
22 |
23 |
24 | <%= submit "Reset password", class: "btn btn-dark w-full" %>
25 |
26 | <% end %>
27 |
28 |
29 | I do rememebr my password
30 |
31 |
32 |
33 | <%= link "Register", to: ~p"/users/register", class: "btn btn-link btn-sm dark:text-blue-500" %>
34 | <%= link "Log in", to: ~p"/users/log_in", class: "btn btn-link btn-sm dark:text-blue-500" %>
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix/jobs/send_chat_reminder_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Jobs.SendChatReminderTest do
2 | import Mox
3 | use StreamClosedCaptionerPhoenix.DataCase, async: true
4 | use Oban.Testing, repo: StreamClosedCaptionerPhoenix.Repo
5 |
6 | alias StreamClosedCaptionerPhoenix.Jobs.SendChatReminder
7 | alias StreamClosedCaptionerPhoenixWeb.UserTracker
8 |
9 | setup do
10 | UserTracker.untrack(self(), "active_channels", "123")
11 | end
12 |
13 | test "it sends a chat message to the broadcaster if the channel is not active" do
14 | Twitch.MockHelix
15 | |> expect(:send_extension_chat_message, fn _creds, "123", message ->
16 | assert message ==
17 | "Hey @talk2megooseman, here is your friendly reminder to turn on Stream Closed Captioner."
18 |
19 | {:ok, %{}}
20 | end)
21 |
22 | # Enqueue a job
23 | assert :ok =
24 | Oban.Testing.perform_job(
25 | SendChatReminder,
26 | %{broadcaster_user_id: "123", broadcaster_user_login: "talk2megooseman"},
27 | []
28 | )
29 |
30 | verify!()
31 | end
32 |
33 | test "it does not send a chat message to the broadcaster if the channel is active" do
34 | UserTracker.track(self(), "active_channels", "123", %{
35 | last_publish: System.system_time(:second)
36 | })
37 |
38 | # Enqueue a job
39 | assert :ok =
40 | Oban.Testing.perform_job(
41 | SendChatReminder,
42 | %{broadcaster_user_id: "123", broadcaster_user_login: "talk2megooseman"},
43 | []
44 | )
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/assets/js/controllers/translations_controller.js:
--------------------------------------------------------------------------------
1 | import debugLogger from "debug"
2 | import { isBrowserCompatible } from "../utils"
3 |
4 | import { ApplicationController } from "stimulus-use"
5 |
6 | const debug = debugLogger("cc:obs-controller")
7 |
8 | export default class extends ApplicationController {
9 | static targets = ["translationStatus", "bitsBalance", "displayTranslations", "translationsList"]
10 |
11 | connect() {
12 | if (isBrowserCompatible()) {
13 | import("../channels").then(this.successfulSocketConnection)
14 | }
15 | }
16 |
17 | disconnect() { }
18 |
19 | successfulSocketConnection = ({ captionsChannel }) => {
20 | this.captionsChannel = captionsChannel
21 |
22 | this.captionsChannel.on("transaction", ({ balance }) => {
23 | this.bitsBalanceTarget.innerHTML = balance
24 | })
25 |
26 | this.captionsChannel.on("translationActivated", ({ enabled, balance }) => {
27 | if (enabled) {
28 | this.translationStatusTarget.innerHTML = "Enabled"
29 | this.bitsBalanceTarget.innerHTML = balance
30 | }
31 | })
32 | }
33 |
34 | onCaptionsReceived = ({ detail: { translations } }) => {
35 | if (translations) {
36 | this.displayTranslationsTarget.classList.remove("hidden")
37 | this.translationsListTarget.innerHTML = ""
38 |
39 | for (const lang in translations) {
40 | const { name, text } = translations[lang];
41 |
42 | const liNode = document.createElement("li")
43 | liNode.classList.add('list-item')
44 | liNode.innerText = `${name}: ${text}`
45 |
46 | this.translationsListTarget.appendChild(liNode)
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/channels/user_tracker_test.exs:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserTrackerTest do
2 | use StreamClosedCaptionerPhoenixWeb.ChannelCase, async: true
3 |
4 | alias StreamClosedCaptionerPhoenixWeb.UserTracker
5 |
6 | # setup do
7 | # on_exit(fn ->
8 | # for pid <- UserTracker.fetchers_pids() do
9 | # ref = Process.monitor(pid)
10 | # assert_receive {:DOWN, ^ref, _, _, _}, 1000
11 | # end
12 | # end)
13 | # end
14 |
15 | setup do
16 | UserTracker.untrack(self(), "active_channels", "123")
17 | end
18 |
19 | test "recently_active_channels/0 return empty list when no channels are active" do
20 | assert UserTracker.recently_active_channels() == []
21 | end
22 |
23 | test "recently_active_channels/0 return list of active channels" do
24 | UserTracker.track(self(), "active_channels", "123", %{
25 | last_publish: System.system_time(:second)
26 | })
27 |
28 | assert UserTracker.recently_active_channels() == ["123"]
29 | end
30 |
31 | test "channel_active?/1 return false when channel is not present" do
32 | assert UserTracker.channel_active?("123") == false
33 | end
34 |
35 | test "channel_active?/1 return false when channel has no activity" do
36 | UserTracker.track(self(), "active_channels", "123", %{})
37 | assert UserTracker.channel_active?("123") == false
38 | end
39 |
40 | test "channel_active?/1 return true when channel was recently active" do
41 | UserTracker.track(self(), "active_channels", "123", %{
42 | last_publish: System.system_time(:second)
43 | })
44 |
45 | assert UserTracker.channel_active?("123") == true
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM hexpm/elixir:1.14.0-erlang-25.0-alpine-3.18.0 AS build
2 |
3 | # install build dependencies
4 | RUN apt-get update -y && apt-get install -y build-essential git npm wget \
5 | && apt-get clean && rm -f /var/lib/apt/lists/*_*
6 |
7 | # prepare build dir
8 | WORKDIR /app
9 |
10 | # install hex + rebar
11 | RUN mix local.hex --force && \
12 | mix local.rebar --force
13 |
14 | # set build ENV
15 | ENV MIX_ENV=prod
16 |
17 | # install mix dependencies
18 | COPY mix.exs mix.lock ./
19 | COPY config config
20 | RUN mix deps.get --only $MIX_ENV
21 | RUN mix deps.compile
22 |
23 | # build assets
24 | COPY assets/package.json assets/package-lock.json ./assets/
25 | RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
26 |
27 | # assets -- copy asset files so purgecss doesnt remove css files
28 | COPY lib/stream_closed_captioner_phoenix_web/live/ lib/stream_closed_captioner_phoenix_web/live/
29 | COPY lib/stream_closed_captioner_phoenix_web/controllers/ lib/stream_closed_captioner_phoenix_web/controllers/
30 |
31 | COPY priv priv
32 | COPY assets assets
33 |
34 | # compile assets
35 | RUN mix assets.deploy
36 | RUN mix phx.digest
37 |
38 | # compile and build release
39 | COPY lib lib
40 | # uncomment COPY if rel/ exists
41 | # COPY rel rel
42 | RUN mix do compile, release
43 |
44 | # prepare release image
45 | FROM alpine:3.9 AS app
46 | RUN apk add --no-cache openssl ncurses-libs
47 |
48 | WORKDIR /app
49 |
50 | RUN chown nobody:nobody /app
51 |
52 | USER nobody:nobody
53 |
54 | COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/stream_closed_captioner_phoenix ./
55 |
56 | ENV HOME=/app
57 |
58 | CMD ["bin/stream_closed_captioner_phoenix", "start"]
59 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/dashboard/indicators/twitch_enabled_indicator.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
Twitch Extension Captions
14 |
15 |
Available and ready to turn on
16 |
17 |
18 |
19 |
20 | <%= _switch_off(assigns) %>
21 |
22 |
23 | <%= _switch_on(assigns) %>
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/azure/cognitive.ex:
--------------------------------------------------------------------------------
1 | defmodule Azure.Cognitive do
2 | import Helpers
3 |
4 | use NewRelic.Tracer
5 |
6 | alias Azure.Cognitive.Translations
7 | alias Ecto.UUID
8 | alias NewRelic.Instrumented.HTTPoison
9 | @behaviour Azure.CognitiveProvider
10 |
11 | @impl Azure.CognitiveProvider
12 |
13 | @trace :translate
14 | def translate(from_language \\ "en", to_languages, text)
15 | when is_list(to_languages) and is_binary(text) do
16 | language_tuple_list =
17 | Enum.flat_map(to_languages, fn lang ->
18 | [code | _] = String.split(from_language, "-")
19 |
20 | if lang != code do
21 | [{:to, lang}]
22 | else
23 | []
24 | end
25 | end)
26 |
27 | params = [
28 | {"api-version", "3.0"},
29 | {:profanityAction, "Marked"},
30 | {:from, from_language} | language_tuple_list
31 | ]
32 |
33 | headers = [
34 | {"Content-Type", "application/json; charset=UTF-8"},
35 | {"Ocp-Apim-Subscription-Key", System.get_env("COGNITIVE_SERVICE_KEY")},
36 | {"Ocp-Apim-Subscription-Region", "westus2"},
37 | {"X-ClientTraceId", UUID.generate()}
38 | ]
39 |
40 | body =
41 | Jason.encode!([
42 | %{
43 | text: text
44 | }
45 | ])
46 |
47 | NewRelic.add_attributes(translate: %{from: from_language, to: to_languages, text: text})
48 |
49 | [translations] =
50 | "https://guzman.codes/azure_proxy/translate"
51 | |> encode_url_and_params(params)
52 | |> HTTPoison.post!(body, headers)
53 | |> Map.fetch!(:body)
54 | |> Jason.decode!()
55 |
56 | Translations.new(translations)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/accounts/user_queries.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Accounts.UserQueries do
2 | import Ecto.Query, warn: false
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts.User
5 | alias StreamClosedCaptionerPhoenix.Repo
6 | alias StreamClosedCaptionerPhoenix.Settings.StreamSettings
7 |
8 | def with_id(query \\ base(), id) do
9 | query
10 | |> where([user], user.id == ^id)
11 | |> limit(1)
12 | end
13 |
14 | def with_ids(query \\ base(), ids) do
15 | query
16 | |> where([user], user.id in ^ids)
17 | end
18 |
19 | def with_provider(query \\ base(), provider) do
20 | query
21 | |> where([user], user.provider == ^provider)
22 | end
23 |
24 | def with_uid(query \\ base(), uid) do
25 | query
26 | |> where([user], user.uid == ^uid)
27 | |> limit(1)
28 | end
29 |
30 | def select_id_user_pair(query \\ base()) do
31 | query
32 | |> select([user], {user.id, user})
33 | |> limit(1)
34 | end
35 |
36 | def with_email(query \\ base(), email) do
37 | query
38 | |> where([user], user.email == ^email)
39 | end
40 |
41 | def query_users_with_settings() do
42 | query =
43 | from(u in User,
44 | join: ss in StreamSettings,
45 | on: ss.user_id == u.id,
46 | select: %{id: u.id}
47 | )
48 |
49 | query
50 | end
51 |
52 | def get_users_without_settings() do
53 | query =
54 | from(u in User,
55 | left_join: ss in StreamSettings,
56 | on: ss.user_id == u.id,
57 | where: is_nil(ss.user_id),
58 | select: u.id
59 | )
60 |
61 | Repo.all(query)
62 | end
63 |
64 | defp base do
65 | from(_ in User, as: :user)
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker image
2 | on:
3 | release:
4 | types: [published]
5 | env:
6 | IMAGE_NAME: stream_closed_captioner_phoenix
7 | jobs:
8 | # Push image to GitHub Packages.
9 | # See also https://docs.docker.com/docker-hub/builds/
10 | push:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | packages: write
14 | contents: read
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Build image
20 | run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}"
21 |
22 | - name: Log into registry
23 | # This is where you will update the PAT to GITHUB_TOKEN
24 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
25 |
26 | - name: Push image
27 | run: |
28 | IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
29 |
30 | # Change all uppercase to lowercase
31 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
32 | # Strip git ref prefix from version
33 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
34 | # Strip "v" prefix from tag name
35 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
36 | # Use Docker `latest` tag convention
37 | [ "$VERSION" == "master" ] && VERSION=latest
38 | echo IMAGE_ID=$IMAGE_ID
39 | echo VERSION=$VERSION
40 | docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
41 | docker tag $IMAGE_NAME $IMAGE_ID:latest
42 | docker push $IMAGE_ID --all-tags
43 | env:
44 | DATABASE_URL: ${{ secrets.DATABASE_URL }}
45 |
--------------------------------------------------------------------------------
/assets/js/controllers/zoom_controller.js:
--------------------------------------------------------------------------------
1 | import debugLogger from "debug"
2 |
3 | import { ApplicationController } from "stimulus-use"
4 |
5 | import { isNil, isEmpty } from "ramda"
6 |
7 | const debug = debugLogger("cc:obs-controller")
8 |
9 | export default class extends ApplicationController {
10 | static targets = ["offButton", "onButton", "errorMarker", "errorMessage"]
11 |
12 | connect() {
13 | this.url = undefined
14 | this.enabled = false
15 | }
16 |
17 | disconnect() { }
18 |
19 | onUrlChange(e) {
20 | this.url = e.target.value
21 | this.dispatch("state", { enabled: this.enabled, url: this.url })
22 | }
23 |
24 | enable = () => {
25 | this.clearErrorMessage()
26 |
27 | if (this.enabled) {
28 | this.onButtonTarget.classList.add("hidden")
29 | this.offButtonTarget.classList.remove("hidden")
30 | this.errorMarkerTarget.classList.add("hidden")
31 | this.enabled = false
32 | } else {
33 | if (isEmpty(this.url) || isNil(this.url)) {
34 | this.errorMarkerTarget.classList.remove("hidden")
35 | this.displayErrorMessage("Please provide a URL for Zoom")
36 | return
37 | }
38 |
39 | this.onButtonTarget.classList.remove("hidden")
40 | this.offButtonTarget.classList.add("hidden")
41 | this.errorMarkerTarget.classList.add("hidden")
42 | this.enabled = true
43 | }
44 |
45 | this.dispatch("state", { enabled: this.enabled, url: this.url })
46 | }
47 |
48 | clearErrorMessage() {
49 | this.errorMessageTarget.classList.add("hidden")
50 | this.errorMessageTarget.innerText = ""
51 | }
52 |
53 | displayErrorMessage(text) {
54 | this.errorMessageTarget.classList.remove("hidden")
55 | this.errorMessageTarget.innerText = text
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/supporters/index.html.heex:
--------------------------------------------------------------------------------
1 | Stream Closed Captioner Supporters
2 |
3 | Twitch Subscribers
4 |
5 | <%= for subscriber <- get_in(@data, ["twitch", "broadcasterSubscriptions"]) |> filter_twitch_subscribers do %>
6 |
7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 |
<%= subscriber["user"]["displayName"] %>
15 |
<%= subscriber["user"]["description"] %>
16 |
17 |
18 |
19 | <% end %>
20 |
21 |
22 | Patrons
23 |
24 | <%= for patron <- get_in(@data, ["patreon", "campaignMembers"]) |> filter_patreon_subscribers || [] do %>
25 |
26 |
27 |
28 |
29 |

30 |
31 |
32 |
33 |
<%= patron["fullName"] %>
34 |
<%= get_polite_status(patron["currentlyEntitledAmountCents"]) %>
35 |
36 |
37 |
38 | <% end %>
39 |
40 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/showcase/index.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 | Discover streamers that are using Stream Closed Captioner on their channels
4 |
5 | <%= if Enum.empty?(@data) do %>
6 |
7 | No Twitch channels are live right now.
8 |
9 | <% end %>
10 |
11 | <%= for user_stream <- @data do %>
12 |
29 | <% end %>
30 |
31 |
32 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_live/index.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Index do
2 | use <%= inspect context.web_module %>, :live_view
3 |
4 | alias <%= inspect context.module %>
5 | alias <%= inspect schema.module %>
6 |
7 | @impl true
8 | def mount(_params, _session, socket) do
9 | {:ok, assign(socket, :<%= schema.collection %>, list_<%= schema.plural %>())}
10 | end
11 |
12 | @impl true
13 | def handle_params(params, _url, socket) do
14 | {:noreply, apply_action(socket, socket.assigns.live_action, params)}
15 | end
16 |
17 | defp apply_action(socket, :edit, %{"id" => id}) do
18 | socket
19 | |> assign(:page_title, "Edit <%= schema.human_singular %>")
20 | |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))
21 | end
22 |
23 | defp apply_action(socket, :new, _params) do
24 | socket
25 | |> assign(:page_title, "New <%= schema.human_singular %>")
26 | |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{})
27 | end
28 |
29 | defp apply_action(socket, :index, _params) do
30 | socket
31 | |> assign(:page_title, "Listing <%= schema.human_plural %>")
32 | |> assign(:<%= schema.singular %>, nil)
33 | end
34 |
35 | @impl true
36 | def handle_event("delete", %{"id" => id}, socket) do
37 | <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
38 | {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
39 |
40 | {:noreply, assign(socket, :<%= schema.collection %>, list_<%=schema.plural %>())}
41 | end
42 |
43 | defp list_<%= schema.plural %> do
44 | <%= inspect context.alias %>.list_<%= schema.plural %>()
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/captions_pipeline/translations.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.CaptionsPipeline.Translations do
2 | use NewRelic.Tracer
3 |
4 | alias Azure.Cognitive.Translations
5 | alias StreamClosedCaptionerPhoenix.Accounts.User
6 | alias StreamClosedCaptionerPhoenix.Bits
7 | alias StreamClosedCaptionerPhoenix.Settings
8 |
9 | @trace :maybe_translate
10 | def maybe_translate(payload, key, %User{} = user) do
11 | text = Map.get(payload, key)
12 |
13 | if Bits.user_active_debit_exists?(user.id) do
14 | %Translations{translations: translations} = get_translations(user, text)
15 | %{payload | translations: translations}
16 | else
17 | to_languages = Settings.get_formatted_translate_languages_by_user(user.id)
18 | bits_balance = Bits.get_bits_balance_for_user(user)
19 |
20 | if Enum.empty?(to_languages) || bits_balance.balance < 500 do
21 | payload
22 | else
23 | activate_translations_for(user, payload, text)
24 | end
25 | end
26 | end
27 |
28 | defp activate_translations_for(%User{} = user, payload, text) do
29 | case Bits.activate_translations_for(user) do
30 | {:ok, _} ->
31 | translations = get_translations(user, text)
32 | %{payload | translations: translations}
33 |
34 | _ ->
35 | payload
36 | end
37 | end
38 |
39 | defp get_translations(%User{} = user, text) do
40 | {:ok, stream_settings} = Settings.get_stream_settings_by_user_id(user.id)
41 |
42 | from_language = stream_settings.language
43 | # Sort so keys are always in same order for consistent hashing
44 | to_languages =
45 | Settings.get_formatted_translate_languages_by_user(user.id) |> Map.keys() |> Enum.sort()
46 |
47 | Azure.perform_translations(from_language, to_languages, text)
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field, opts \\ %{}) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
13 | content_tag(:span, translate_error(error),
14 | class: opts[:class],
15 | phx_feedback_for: input_id(form, field)
16 | )
17 | end)
18 | end
19 |
20 | @doc """
21 | Translates an error message using gettext.
22 | """
23 | def translate_error({msg, opts}) do
24 | # When using gettext, we typically pass the strings we want
25 | # to translate as a static argument:
26 | #
27 | # # Translate "is invalid" in the "errors" domain
28 | # dgettext("errors", "is invalid")
29 | #
30 | # # Translate the number of files with plural rules
31 | # dngettext("errors", "1 file", "%{count} files", count)
32 | #
33 | # Because the error messages we show in our forms and APIs
34 | # are defined inside Ecto, we need to translate them dynamically.
35 | # This requires us to call the Gettext module passing our gettext
36 | # backend as first argument.
37 | #
38 | # Note we use the "errors" domain, which means translations
39 | # should be written to the errors.po file. The :count option is
40 | # set by Ecto and indicates we should also apply plural rules.
41 | if count = opts[:count] do
42 | Gettext.dngettext(StreamClosedCaptionerPhoenixWeb.Gettext, "errors", msg, msg, count, opts)
43 | else
44 | Gettext.dgettext(StreamClosedCaptionerPhoenixWeb.Gettext, "errors", msg, opts)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/live/transcirpts_live/show.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 | <%= Enum.join(@final_list, ". ") %>
14 |
15 |
16 |
17 | <%= @interim %>
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/oauth.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Oauth do
2 | import Helpers
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts.User
5 | alias Twitch.Helix.Credentials
6 | alias Twitch.Parser
7 | alias NewRelic.Instrumented.HTTPoison
8 |
9 | def get_client_access_token() do
10 | credentials = get_credentials()
11 |
12 | headers = [
13 | {"Content-Type", "application/json"},
14 | {"Client-Id", credentials.client_id}
15 | ]
16 |
17 | params = %{
18 | client_id: credentials.client_id,
19 | client_secret: credentials.client_secret,
20 | grant_type: "client_credentials",
21 | scope: ""
22 | }
23 |
24 | access_token =
25 | encode_url_and_params("https://id.twitch.tv/oauth2/token", params)
26 | |> HTTPoison.post!("", headers)
27 | |> Map.fetch!(:body)
28 | |> Jason.decode!()
29 | |> get_in(["access_token"])
30 |
31 | Map.put(credentials, :access_token, access_token)
32 | end
33 |
34 | def get_users_access_token(%User{} = user) do
35 | credentials = get_credentials()
36 |
37 | case validate_token(user.access_token) do
38 | {:ok, _} ->
39 | Map.put(credentials, :access_token, user.access_token)
40 |
41 | {:error, _} ->
42 | nil
43 | # Refresh token
44 | end
45 | end
46 |
47 | defp validate_token(access_token) do
48 | headers = [
49 | {"Authorization", "OAuth #{access_token}"}
50 | ]
51 |
52 | encode_url_and_params("https://id.twitch.tv/oauth2/validate")
53 | |> HTTPoison.get(headers)
54 | |> Parser.parse()
55 | end
56 |
57 | defp get_credentials,
58 | do: %Credentials{
59 | client_id: Application.get_env(:stream_closed_captioner_phoenix, :twitch_client_id),
60 | client_secret: Application.get_env(:stream_closed_captioner_phoenix, :twitch_client_secret)
61 | }
62 | end
63 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/helix_provider.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.HelixProvider do
2 | alias Twitch.Helix.{Credentials, Stream, Transaction}
3 |
4 | @callback get_streams(
5 | Credentials.t(),
6 | list(String.t()),
7 | String.t() | nil
8 | ) :: list(Stream.t())
9 |
10 | @callback get_transactions(Credentials.t()) :: list(Transaction.t())
11 |
12 | @callback get_users_active_extensions(Credentials.t()) :: map()
13 |
14 | @callback send_extension_chat_message(Twitch.Extension.Credentials.t(), String.t(), String.t()) ::
15 | tuple()
16 |
17 | @callback get_live_channels(
18 | Credentials.t(),
19 | String.t() | nil
20 | ) :: [Channel.t()]
21 |
22 | @callback set_configuration_for(
23 | Twitch.Extension.Credentials.t(),
24 | atom(),
25 | String.t(),
26 | map()
27 | ) :: any
28 |
29 | @callback get_configuration_for(
30 | Twitch.Extension.Credentials.t(),
31 | atom(),
32 | String.t()
33 | ) :: {:ok, HTTPoison.Response.t()}
34 |
35 | @callback eventsub_subscribe(
36 | Credentials.t(),
37 | String.t(),
38 | String.t(),
39 | String.t(),
40 | String.t()
41 | ) :: {:ok, HTTPoison.Response.t()}
42 |
43 | @callback get_eventsub_subscriptions(
44 | %{:access_token => binary, optional(any) => any},
45 | String.t(),
46 | String.t() | nil
47 | ) ::
48 | list
49 |
50 | @callback delete_eventsub_subscription(
51 | %{
52 | :access_token => binary,
53 | optional(any) => any
54 | },
55 | String.t()
56 | ) :: Integer.t()
57 | end
58 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/services/twitch/jwt.ex:
--------------------------------------------------------------------------------
1 | defmodule Twitch.Jwt do
2 | alias Twitch.Extension.Credentials
3 | alias Twitch.Extension.Token
4 |
5 | def get_credentials,
6 | do: %Credentials{
7 | client_id: System.get_env("TWITCH_CLIENT_ID") || "",
8 | token_secret: Application.get_env(:stream_closed_captioner_phoenix, :twitch_token_secret)
9 | }
10 |
11 | def verify_and_validate(token) do
12 | credentials = get_credentials()
13 | signer = create_signer(credentials)
14 |
15 | Token.verify_and_validate(token, signer)
16 | end
17 |
18 | @spec sign_token_for(:pubsub | :standard, String.t()) :: %Twitch.Extension.Credentials{
19 | client_id: binary,
20 | token_secret: binary,
21 | jwt_token: term()
22 | }
23 | def sign_token_for(:standard, channel_id) do
24 | credentials = get_credentials()
25 | signer = create_signer(credentials)
26 |
27 | claims = %{
28 | "role" => "external",
29 | "channel_id" => channel_id,
30 | "user_id" => "120750024"
31 | }
32 |
33 | token_with_claims = Token.generate_and_sign!(claims, signer)
34 | Map.put(credentials, :jwt_token, token_with_claims)
35 | end
36 |
37 | def sign_token_for(:pubsub, channel_id) do
38 | credentials = get_credentials()
39 | signer = create_signer(credentials)
40 |
41 | claims = %{
42 | "role" => "external",
43 | "channel_id" => channel_id,
44 | "user_id" => "120750024",
45 | "pubsub_perms" => %{
46 | "send" => [
47 | "broadcast"
48 | ]
49 | }
50 | }
51 |
52 | token_with_claims = Token.generate_and_sign!(claims, signer)
53 | Map.put(credentials, :jwt_token, token_with_claims)
54 | end
55 |
56 | defp create_signer(credentials) do
57 | secret = credentials.token_secret |> Base.decode64!()
58 | Joken.Signer.create("HS256", secret)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/bits/bits_transaction_queries.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Bits.BitsTransactionQueries do
2 | import Ecto.Query, warn: false
3 |
4 | alias StreamClosedCaptionerPhoenix.Bits.BitsBalanceDebit
5 | alias StreamClosedCaptionerPhoenix.Bits.BitsTransaction
6 |
7 | def all(query \\ base()), do: query
8 |
9 | @spec with_user_id(any, any) :: Ecto.Query.t()
10 | def with_user_id(query \\ base(), user_id) do
11 | query
12 | |> where([bits_transaction], bits_transaction.user_id == ^user_id)
13 | end
14 |
15 | @spec with_id(any, any) :: Ecto.Query.t()
16 | def with_id(query \\ base(), id) do
17 | query
18 | |> where([bits_transaction], bits_transaction.id == ^id)
19 | end
20 |
21 | @spec with_transaction_id(any, String.t()) :: Ecto.Query.t()
22 | def with_transaction_id(query \\ base(), transaction_id) do
23 | query
24 | |> where([bits_transaction], bits_transaction.transaction_id == ^transaction_id)
25 | end
26 |
27 | defp base do
28 | from(_ in BitsTransaction, as: :bits_transaction)
29 | end
30 |
31 | def get_bits_transactions_and_debits_for_user(user_id) do
32 | transaction_query =
33 | from(t in BitsTransaction,
34 | where: t.user_id == ^user_id,
35 | select: %{
36 | amount: t.amount,
37 | time: t.time,
38 | id: t.transaction_id,
39 | purchaser_id: t.purchaser_uid,
40 | action: "purchase"
41 | }
42 | )
43 |
44 | union_all_query =
45 | from(d in BitsBalanceDebit,
46 | where: d.user_id == ^user_id,
47 | select: %{
48 | amount: d.amount,
49 | time: d.created_at,
50 | id: "",
51 | purchaser_id: "",
52 | action: "debit"
53 | },
54 | union_all: ^transaction_query
55 | )
56 |
57 | from(s in subquery(union_all_query), order_by: [desc: s.time])
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/priv/templates/phx.gen.pretty_html/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 | <%= for {k, _} <- schema.attrs do %> | <%= Phoenix.Naming.humanize(Atom.to_string(k)) %> |
17 | <% end %>
18 | |
19 |
20 |
21 |
22 | <%%= for <%= schema.singular %> <- @<%= schema.plural %> do %>
23 |
24 | <%= for {k, _} <- schema.attrs do %> | <%%= <%= schema.singular %>.<%= k %> %> |
25 | <% end %>
26 |
27 | <%%= link "Show", to: Routes.<%= schema.route_helper %>_path(@conn, :show, <%= schema.singular %>) %>
28 | <%%= link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, <%= schema.singular %>) %>
29 | <%%= link "Delete", to: Routes.<%= schema.route_helper %>_path(@conn, :delete, <%= schema.singular %>), method: :delete, data: [confirm: "Are you sure?"] %>
30 | |
31 |
32 | <%% end %>
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/settings/translate_language.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Settings.TranslateLanguage do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | import Ecto.Query, warn: false
5 |
6 | alias StreamClosedCaptionerPhoenix.Repo
7 |
8 | schema "translate_languages" do
9 | field :language, :string
10 | belongs_to :user, StreamClosedCaptionerPhoenix.Accounts.User
11 |
12 | timestamps(inserted_at: :created_at)
13 | end
14 |
15 | @doc false
16 | def changeset(translate_language, attrs) do
17 | translate_language
18 | |> cast(attrs, [:user_id, :language])
19 | |> foreign_key_constraint(:user_id, name: "fk_rails_e519515539")
20 | |> unique_constraint([:language, :user_id],
21 | name: "index_translate_languages_on_user_id_and_language"
22 | )
23 | |> validate_required([:user_id, :language])
24 | |> validate_inclusion(:language, StreamClosedCaptionerPhoenix.Settings.valid_language_codes())
25 | |> validate_language_count()
26 | end
27 |
28 | @doc false
29 | def update_changeset(translate_language, attrs) do
30 | translate_language
31 | |> cast(attrs, [:language])
32 | |> validate_required([:language])
33 | |> unique_constraint([:language, :user_id],
34 | name: "index_translate_languages_on_user_id_and_language"
35 | )
36 | |> validate_language_count()
37 | |> validate_inclusion(:language, StreamClosedCaptionerPhoenix.Settings.valid_language_codes())
38 | end
39 |
40 | @doc false
41 | defp validate_language_count(changeset) do
42 | with %{user_id: user_id} <- changeset.changes do
43 | count =
44 | StreamClosedCaptionerPhoenix.Settings.TranslateLanguage
45 | |> where(user_id: ^user_id)
46 | |> Repo.count()
47 |
48 | if count >= 3 do
49 | add_error(changeset, :language, "cannot have more than 3 languages")
50 | else
51 | changeset
52 | end
53 | else
54 | _ -> changeset
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/accounts/user_notifier.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Accounts.UserNotifier do
2 | import Bamboo.Email
3 |
4 | defp deliver(to, subject, body) do
5 | require Logger
6 | Logger.debug(body)
7 |
8 | new_email(
9 | to: to,
10 | from: "erik.guzman@guzman.codes",
11 | subject: subject,
12 | text_body: body
13 | )
14 | |> StreamClosedCaptionerPhoenix.Mailer.deliver_now()
15 | end
16 |
17 | @doc """
18 | Deliver instructions to confirm account.
19 | """
20 | def deliver_confirmation_instructions(user, url) do
21 | deliver(user.email, "Confirm your account", """
22 |
23 | ==============================
24 |
25 | Hi #{user.email},
26 |
27 | You can confirm your account by visiting the URL below:
28 |
29 | #{url}
30 |
31 | If you didn't create an account with us, please ignore this.
32 |
33 | ==============================
34 | """)
35 | end
36 |
37 | @doc """
38 | Deliver instructions to reset a user password.
39 | """
40 | def deliver_reset_password_instructions(user, url) do
41 | deliver(user.email, "Stream CC Password Reset Instructions", """
42 |
43 | ==============================
44 |
45 | Hi #{user.email},
46 |
47 | You can reset your password by visiting the URL below:
48 |
49 | #{url}
50 |
51 | If you didn't request this change, please ignore this.
52 |
53 | ==============================
54 | """)
55 | end
56 |
57 | @doc """
58 | Deliver instructions to update a user email.
59 | """
60 | def deliver_update_email_instructions(user, url) do
61 | deliver(user.email, "Stream CC Update Email Instructions", """
62 |
63 | ==============================
64 |
65 | Hi #{user.email},
66 |
67 | You can change your email by visiting the URL below:
68 |
69 | #{url}
70 |
71 | If you didn't request this change, please ignore this.
72 |
73 | ==============================
74 | """)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_confirmation_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserConfirmationController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 |
6 | def new(conn, _params) do
7 | render(conn, "new.html")
8 | end
9 |
10 | def create(conn, %{"user" => %{"email" => email}}) do
11 | if user = Accounts.get_user_by_email(email) do
12 | Accounts.deliver_user_confirmation_instructions(
13 | user,
14 | &Routes.user_confirmation_url(conn, :confirm, &1)
15 | )
16 | end
17 |
18 | # Regardless of the outcome, show an impartial success/error message.
19 | conn
20 | |> put_flash(
21 | :info,
22 | "If your email is in our system and it has not been confirmed yet, " <>
23 | "you will receive an email with instructions shortly."
24 | )
25 | |> redirect(to: "/")
26 | end
27 |
28 | # Do not log in the user after confirmation to avoid a
29 | # leaked token giving the user access to the account.
30 | def confirm(conn, %{"token" => token}) do
31 | case Accounts.confirm_user(token) do
32 | {:ok, _} ->
33 | conn
34 | |> put_flash(:info, "Account confirmed successfully.")
35 | |> redirect(to: "/")
36 |
37 | :error ->
38 | # If there is a current user and the account was already confirmed,
39 | # then odds are that the confirmation link was already visited, either
40 | # by some automation or by the user themselves, so we redirect without
41 | # a warning message.
42 | case conn.assigns do
43 | %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
44 | redirect(conn, to: "/")
45 |
46 | %{} ->
47 | conn
48 | |> put_flash(:error, "Account confirmation link is invalid or it has expired.")
49 | |> redirect(to: "/")
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | we enable the SQL sandbox, so changes done to the database
11 | are reverted at the end of every test. If you are using
12 | PostgreSQL, you can even run database tests asynchronously
13 | by setting `use StreamClosedCaptionerPhoenix.DataCase, async: true`, although
14 | this option is not recommended for other databases.
15 | """
16 |
17 | use ExUnit.CaseTemplate
18 |
19 | using do
20 | quote do
21 | alias StreamClosedCaptionerPhoenix.Repo
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | import StreamClosedCaptionerPhoenix.DataCase
27 | end
28 | end
29 |
30 | setup tags do
31 | StreamClosedCaptionerPhoenix.DataCase.setup_sandbox(tags)
32 |
33 | :ok
34 | end
35 |
36 | @doc """
37 | Sets up the sandbox based on the test tags.
38 | """
39 | def setup_sandbox(tags) do
40 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(StreamClosedCaptionerPhoenix.Repo, shared: not tags[:async])
41 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
42 | end
43 |
44 | @doc """
45 | A helper that transforms changeset errors into a map of messages.
46 |
47 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
48 | assert "password is too short" in errors_on(changeset).password
49 | assert %{password: ["password is too short"]} = errors_on(changeset)
50 |
51 | """
52 | def errors_on(changeset) do
53 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
54 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
55 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
56 | end)
57 | end)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/error/404.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= csrf_meta_tag() %>
8 | <.live_title suffix=" · Closed Captions">
9 | <%= assigns[:page_title] || "Stream Closed Captioner" %>
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Error 404
27 |
28 |
32 |
33 | You might have the wrong address, or the page may
34 | have moved.
35 |
36 |
37 | Back to homepage
38 |
39 |
40 |
41 |
42 |

46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/channels/active_presence.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.ActivePresence do
2 | @moduledoc """
3 | Provides presence tracking to channels and processes.
4 |
5 | See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
6 | docs for more details.
7 | """
8 |
9 | # alias StreamClosedCaptionerPhoenix.Accounts
10 |
11 | @active_time_out 700
12 |
13 | use Phoenix.Presence,
14 | otp_app: :stream_closed_captioner_phoenix,
15 | pubsub_server: StreamClosedCaptionerPhoenix.PubSub
16 |
17 | # def fetch(_topic, presences) do
18 | # users = presences |> Map.keys() |> Accounts.get_users_map()
19 |
20 | # for {key, %{metas: metas}} <- presences, into: %{} do
21 | # {key, %{metas: metas, user: users[String.to_integer(key)]}}
22 | # end
23 | # end
24 |
25 | def recently_active_channels do
26 | StreamClosedCaptionerPhoenixWeb.ActivePresence.list("active_channels")
27 | |> Enum.reduce([], &reduced_user_list/2)
28 | end
29 |
30 | def channel_active?(channel_id) do
31 | StreamClosedCaptionerPhoenixWeb.ActivePresence.get_by_key("active_channels", channel_id)
32 | |> channel_recently_published?()
33 | end
34 |
35 | defp reduced_user_list({uid, %{metas: metas}}, acc) when is_binary(uid) do
36 | elapased_time = current_timestamp() - get_last_publish(metas)
37 |
38 | if currently_active(elapased_time) do
39 | [uid | acc]
40 | else
41 | acc
42 | end
43 | end
44 |
45 | defp reduced_user_list(_, acc), do: acc
46 |
47 | defp channel_recently_published?(%{metas: metas}) do
48 | elapased_time = current_timestamp() - get_last_publish(metas)
49 | currently_active(elapased_time)
50 | end
51 |
52 | defp channel_recently_published?([]), do: false
53 |
54 | defp current_timestamp, do: System.system_time(:second)
55 |
56 | defp get_last_publish(metas),
57 | do: metas |> List.first() |> Map.get(:last_publish, current_timestamp())
58 |
59 | defp currently_active(elapsed_time), do: elapsed_time <= @active_time_out
60 | end
61 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix/settings/stream_settings_admin.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenix.Settings.StreamSettingsAdmin do
2 | alias StreamClosedCaptionerPhoenix.Accounts
3 | alias StreamClosedCaptionerPhoenix.Accounts.UserQueries
4 |
5 | def search_fields(_schema) do
6 | [
7 | user: [:email, :username, :uid]
8 | ]
9 | end
10 |
11 | def task_stream_settings() do
12 | [
13 | # %{
14 | # name: "Users with out Stream Settings",
15 | # initial_value: 0,
16 | # every: 15,
17 | # action: fn _v ->
18 | # user_ids = UserQueries.get_users_without_settings()
19 | # {:ok, user_ids}
20 | # end
21 | # }
22 | ]
23 | end
24 |
25 | def widgets(_schema, _conn) do
26 | [
27 | %{
28 | type: "tidbit",
29 | title: "Users with out Stream Settings",
30 | content: UserQueries.get_users_without_settings() |> Enum.count(),
31 | order: 1,
32 | width: 4,
33 | icon: ''
34 | }
35 | ]
36 | end
37 |
38 | def plural_name(_) do
39 | "Stream Settings"
40 | end
41 |
42 | def ordering(_schema) do
43 | [asc: :id]
44 | end
45 |
46 | def get_user(%{user_id: id}) do
47 | id
48 | |> Accounts.get_user!()
49 | |> Map.get(:username)
50 | end
51 |
52 | def index(_) do
53 | [
54 | user_id: %{name: "User", value: fn p -> get_user(p) end},
55 | caption_delay: nil,
56 | cc_box_size: nil,
57 | filter_profanity: nil,
58 | hide_text_on_load: nil,
59 | language: nil,
60 | pirate_mode: nil,
61 | showcase: nil,
62 | switch_settings_position: nil,
63 | text_uppercase: nil
64 | ]
65 | end
66 |
67 | def form_fields(_) do
68 | [
69 | user_id: %{update: :readonly},
70 | caption_delay: nil,
71 | cc_box_size: nil,
72 | filter_profanity: nil,
73 | hide_text_on_load: nil,
74 | language: nil,
75 | pirate_mode: nil,
76 | showcase: nil,
77 | switch_settings_position: nil,
78 | text_uppercase: nil
79 | ]
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/test/stream_closed_captioner_phoenix_web/live/stream_settings_live_test.exs:
--------------------------------------------------------------------------------
1 | # # defmodule StreamClosedCaptionerPhoenixWeb.CaptionSettingsLiveTest do
2 | # use StreamClosedCaptionerPhoenixWeb.ConnCase
3 |
4 | # import StreamClosedCaptionerPhoenix.Factory
5 | # import Phoenix.LiveViewTest
6 |
7 | # alias StreamClosedCaptionerPhoenix.Settings
8 |
9 | # @create_attrs %{caption_delay: 42, filter_profanity: true}
10 | # @update_attrs %{caption_delay: 43, filter_profanity: false}
11 | # @invalid_attrs %{caption_delay: nil, filter_profanity: nil}
12 |
13 | # defp fixture(:stream_settings) do
14 | # insert(:stream_settings)
15 | # end
16 |
17 | # defp create_stream_settings(_) do
18 | # stream_settings = fixture(:stream_settings)
19 | # %{stream_settings: stream_settings}
20 | # end
21 |
22 | # describe "Show" do
23 | # setup [:create_stream_settings]
24 |
25 | # test "displays stream_settings", %{conn: conn, stream_settings: stream_settings} do
26 | # {:ok, _show_live, html} =
27 | # live(conn, Routes.stream_settings_show_path(conn, :show, stream_settings))
28 |
29 | # assert html =~ "Show Stream settings"
30 | # end
31 |
32 | # test "updates stream_settings within modal", %{conn: conn, stream_settings: stream_settings} do
33 | # {:ok, show_live, _html} =
34 | # live(conn, Routes.stream_settings_show_path(conn, :show, stream_settings))
35 |
36 | # assert show_live |> element("a", "Edit") |> render_click() =~
37 | # "Edit Stream settings"
38 |
39 | # assert_patch(show_live, Routes.stream_settings_show_path(conn, :edit, stream_settings))
40 |
41 | # assert show_live
42 | # |> form("#stream_settings-form", stream_settings: @invalid_attrs)
43 | # |> render_change() =~ "can't be blank"
44 |
45 | # {:ok, _, html} =
46 | # show_live
47 | # |> form("#stream_settings-form", stream_settings: @update_attrs)
48 | # |> render_submit()
49 | # |> follow_redirect(conn, Routes.stream_settings_show_path(conn, :show, stream_settings))
50 |
51 | # assert html =~ "Stream settings updated successfully"
52 | # end
53 | # end
54 | # end
55 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_reset_password_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserResetPasswordController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 |
4 | alias StreamClosedCaptionerPhoenix.Accounts
5 |
6 | plug :get_user_by_reset_password_token when action in [:edit, :update]
7 |
8 | def new(conn, _params) do
9 | render(conn, "new.html")
10 | end
11 |
12 | def create(conn, %{"user" => %{"email" => email}}) do
13 | if user = Accounts.get_user_by_email(email) do
14 | uri = %URI{scheme: "https", host: "stream-cc.gooseman.codes"}
15 |
16 | Accounts.deliver_user_reset_password_instructions(
17 | user,
18 | &Routes.user_reset_password_url(uri, :edit, &1)
19 | )
20 | end
21 |
22 | # Regardless of the outcome, show an impartial success/error message.
23 | conn
24 | |> put_flash(
25 | :info,
26 | "If your email is in our system, you will receive instructions to reset your password shortly."
27 | )
28 | |> redirect(to: "/")
29 | end
30 |
31 | def edit(conn, _params) do
32 | render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user))
33 | end
34 |
35 | # Do not log in the user after reset password to avoid a
36 | # leaked token giving the user access to the account.
37 | def update(conn, %{"user" => user_params}) do
38 | case Accounts.reset_user_password(conn.assigns.user, user_params) do
39 | {:ok, _} ->
40 | conn
41 | |> put_flash(:info, "Password reset successfully.")
42 | |> redirect(to: Routes.user_session_path(conn, :new))
43 |
44 | {:error, changeset} ->
45 | render(conn, "edit.html", changeset: changeset)
46 | end
47 | end
48 |
49 | defp get_user_by_reset_password_token(conn, _opts) do
50 | %{"token" => token} = conn.params
51 |
52 | if user = Accounts.get_user_by_reset_password_token(token) do
53 | conn |> assign(:user, user) |> assign(:token, token)
54 | else
55 | conn
56 | |> put_flash(:error, "Reset password link is invalid or it has expired.")
57 | |> redirect(to: "/")
58 | |> halt()
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/components/layouts/_user_dropdown.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
11 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/controllers/user_session_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserSessionController do
2 | use StreamClosedCaptionerPhoenixWeb, :controller
3 | plug(Ueberauth)
4 |
5 | # alias Ueberauth.Strategy.Helpers
6 |
7 | alias StreamClosedCaptionerPhoenix.Accounts
8 | alias StreamClosedCaptionerPhoenix.AccountsOauth
9 | alias StreamClosedCaptionerPhoenixWeb.UserAuth
10 |
11 | def new(conn, _params) do
12 | render(conn, "new.html", error_message: nil)
13 | end
14 |
15 | def create(conn, %{"user" => user_params}) do
16 | %{"email" => email, "password" => password} = user_params
17 |
18 | if user = Accounts.get_user_by_email_and_password(email, password) do
19 | UserAuth.log_in_user(conn, user, user_params)
20 | else
21 | render(conn, "new.html", error_message: "Invalid email or password")
22 | end
23 | end
24 |
25 | def delete(conn, _params) do
26 | conn
27 | |> put_flash(:info, "Logged out successfully.")
28 | |> UserAuth.log_out_user()
29 | end
30 |
31 | def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
32 | conn
33 | |> put_flash(:error, "Failed to authenticate.")
34 | |> redirect(to: "/")
35 | end
36 |
37 | def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
38 | current_user = conn.assigns.current_user
39 | %{extra: %{raw_info: %{user: user}}, credentials: creds} = auth
40 | [user_info] = user["data"]
41 |
42 | map_creds = Map.from_struct(creds)
43 |
44 | case AccountsOauth.find_or_register_user_with_oauth(user_info, map_creds, current_user) do
45 | {:ok, %{user: user}} ->
46 | conn
47 | |> put_flash(:info, "Successfully authenticated.")
48 | |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
49 |
50 | {:error, :user, _user_changeset, _} ->
51 | conn
52 | |> put_flash(
53 | :error,
54 | "Could not sign up with Twitch. Ensure you setup your account with an email."
55 | )
56 | |> redirect(to: "/")
57 |
58 | {:error, reason} ->
59 | conn
60 | |> put_flash(:error, reason)
61 | |> redirect(to: "/")
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/config/dev.secret.exs:
--------------------------------------------------------------------------------
1 | # In this file, we load production configuration and secrets
2 | # from environment variables. You can also hardcode secrets,
3 | # although such is generally not recommended and you have to
4 | # remember to add this file to your .gitignore.
5 | import Config
6 |
7 | database_url = System.get_env("DATABASE_URL")
8 |
9 | config :stream_closed_captioner_phoenix, StreamClosedCaptionerPhoenix.Repo,
10 | # ssl: true,
11 | url: database_url,
12 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
13 | # The App was started from Rails which used the `schema_migrations` table with the same name but different schema
14 | # To continue with migrations from ecto from now on, we use choose a custom name for the ecto migrations
15 | # !!! From now on, migrations should only be done from Ecto !!!
16 | migration_source: "ecto_schema_migrations"
17 |
18 | secret_key_base = System.get_env("SECRET_KEY_BASE")
19 |
20 | config :stream_closed_captioner_phoenix, StreamClosedCaptionerPhoenixWeb.Endpoint,
21 | http: [
22 | port: String.to_integer(System.get_env("PORT") || "4000"),
23 | transport_options: [socket_opts: [:inet6]]
24 | ],
25 | secret_key_base: secret_key_base
26 |
27 | config :ueberauth, Ueberauth.Strategy.Twitch.OAuth,
28 | client_id: System.get_env("TWITCH_CLIENT_ID"),
29 | client_secret: System.get_env("TWITCH_CLIENT_SECRET"),
30 | redirect_uri: "http://localhost:4000/auth/twitch/callback"
31 |
32 | # config :goth,
33 | # json: System.get_env("BAMBOO_EMAIL_CREDS")
34 |
35 | config :joken, default_signer: System.get_env("TWITCH_TOKEN_SECRET")
36 |
37 | config :stream_closed_captioner_phoenix,
38 | api_key: System.get_env("NOTION_API_KEY"),
39 | notion_version: System.get_env("NOTION_VERSION")
40 |
41 | config :stream_closed_captioner_phoenix,
42 | eventsub_callback_url: System.get_env("EVENTSUB_CALLBACK_URL")
43 |
44 | # ## Using releases (Elixir v1.9+)
45 | #
46 | # If you are doing OTP releases, you need to instruct Phoenix
47 | # to start each relevant endpoint:
48 | #
49 | # config :stream_closed_captioner_phoenix, StreamClosedCaptionerPhoenixWeb.Endpoint, server: true
50 | #
51 | # Then you can assemble a release by calling `mix release`.
52 | # See `mix help release` for more information.
53 |
--------------------------------------------------------------------------------
/lib/stream_closed_captioner_phoenix_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule StreamClosedCaptionerPhoenixWeb.UserSocket do
2 | use Phoenix.Socket
3 | use Absinthe.Phoenix.Socket, schema: StreamClosedCaptionerPhoenixWeb.Schema
4 |
5 | alias StreamClosedCaptionerPhoenix.Accounts
6 |
7 | ## Channels
8 | # channel "room:*", StreamClosedCaptionerPhoenixWeb.RoomChannel
9 |
10 | channel("captions:*", StreamClosedCaptionerPhoenixWeb.CaptionsChannel)
11 |
12 | # Socket params are passed from the client and can
13 | # be used to verify and authenticate a user. After
14 | # verification, you can put default assigns into
15 | # the socket that will be set for all channels, ie
16 | #
17 | # {:ok, assign(socket, :user_id, verified_user_id)}
18 | #
19 | # To deny connection, return `:error`.
20 | #
21 | # See `Phoenix.Token` documentation for examples in
22 | # performing token verification on connect.
23 | @impl true
24 | def connect(%{"token" => token}, socket, _connect_info) do
25 | case Phoenix.Token.verify(socket, "user socket", token, max_age: 5_184_000) do
26 | {:ok, user_id} ->
27 | current_user = Accounts.get_user!(user_id)
28 | {:ok, assign(socket, :current_user, current_user)}
29 |
30 | {:error, _} ->
31 | :error
32 | end
33 | end
34 |
35 | def connect(%{"Authorization" => "Bearer " <> token} = _params, socket, _connect_info) do
36 | case Twitch.Jwt.verify_and_validate(token) do
37 | {:ok, _} ->
38 | socket = Absinthe.Phoenix.Socket.put_options(socket, context: %{})
39 | {:ok, socket}
40 |
41 | {:error, _} ->
42 | :error
43 | end
44 | end
45 |
46 | def connect(_params, _socket, _connect_info), do: :error
47 |
48 | # Socket id's are topics that allow you to identify all sockets for a given user:
49 | #
50 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
51 | #
52 | # Would allow you to broadcast a "disconnect" event and terminate
53 | # all active sockets and channels for a given user:
54 | #
55 | # StreamClosedCaptionerPhoenixWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
56 | #
57 | # Returning `nil` makes this socket anonymous.
58 | @impl true
59 | def id(_socket), do: nil
60 | end
61 |
--------------------------------------------------------------------------------