Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions lux/guides/youtube_community.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# YouTube Community Management Guide

## Comment Manager

```elixir
{:ok, pid} = CommentManager.start_link()

# Analyze comments (sentiment, spam, language, questions)
{:ok, analysis} = CommentManager.analyze_comment(pid, %{id: "c1", text: "Amazing video!", author: "Alice"})
# %{sentiment: :positive, is_spam: false, is_question: false, language: "en"}

# Auto-generate responses
{:ok, resp} = CommentManager.generate_response(pid, %{text: "How does this work?"})
# %{response: %{action: :reply, text: "Great question! Let me look into that."}}

# Batch analysis
{:ok, results} = CommentManager.batch_analyze(pid, comments)
{:ok, stats} = CommentManager.get_stats(pid)
```

## Post Scheduler

```elixir
{:ok, pid} = PostScheduler.start_link()

# Schedule community posts
PostScheduler.schedule_post(pid, %{text: "New video tomorrow!", scheduled_at: ~U[2026-03-10 12:00:00Z]})

# Cross-platform
PostScheduler.schedule_post(pid, %{text: "Big news!", platforms: [:youtube, :twitter]})

# Campaigns
PostScheduler.create_campaign(pid, %{name: "Summer Launch", start_date: ~D[2026-06-01]})

# Get due posts
{:ok, due} = PostScheduler.get_due_posts(pid)
```

## Spam Detector

```elixir
result = SpamDetector.detect("Sub4sub check my channel!")
# %{is_spam: true, score: 75, severity: :critical, recommended_action: :ban_and_delete}

batch = SpamDetector.batch_detect(["Good comment", "SPAM sub4sub click bit.ly/x"])
# %{total: 2, spam_count: 1, spam_rate: 50.0}
```
126 changes: 126 additions & 0 deletions lux/lib/lux/prisms/youtube/community/comment_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
defmodule Lux.Prisms.YouTube.Community.CommentManager do
@moduledoc """
Comment analysis, response generation, and management for YouTube.
"""

use GenServer

defstruct [:comments, :responses, :stats]

def start_link(opts \\ []) do
name = opts[:name] || __MODULE__
GenServer.start_link(__MODULE__, opts, name: name)
end

def analyze_comment(pid \\ __MODULE__, comment) do
GenServer.call(pid, {:analyze, comment})
end

def generate_response(pid \\ __MODULE__, comment) do
GenServer.call(pid, {:generate_response, comment})
end

def batch_analyze(pid \\ __MODULE__, comments) do
GenServer.call(pid, {:batch_analyze, comments})
end

def get_stats(pid \\ __MODULE__) do
GenServer.call(pid, :stats)
end

@impl true
def init(_opts) do
{:ok, %__MODULE__{comments: [], responses: [], stats: %{total: 0, positive: 0, negative: 0, neutral: 0, spam: 0}}}
end

@impl true
def handle_call({:analyze, comment}, _from, state) do
analysis = do_analyze(comment)
stats = update_stats(state.stats, analysis)
{:reply, {:ok, analysis}, %{state | comments: [analysis | state.comments], stats: stats}}
end

@impl true
def handle_call({:generate_response, comment}, _from, state) do
analysis = do_analyze(comment)
response = build_response(analysis)
{:reply, {:ok, %{comment: comment, analysis: analysis, response: response}},
%{state | responses: [response | state.responses]}}
end

@impl true
def handle_call({:batch_analyze, comments}, _from, state) do
results = Enum.map(comments, &do_analyze/1)
stats = Enum.reduce(results, state.stats, &update_stats(&2, &1))
{:reply, {:ok, results}, %{state | comments: results ++ state.comments, stats: stats}}
end

@impl true
def handle_call(:stats, _from, state) do
{:reply, {:ok, state.stats}, state}
end

defp do_analyze(comment) do
text = comment[:text] || ""
sentiment = analyze_sentiment(text)
is_spam = is_spam?(text)
is_question = String.contains?(text, "?")

%{
id: comment[:id],
text: text,
author: comment[:author],
sentiment: sentiment,
is_spam: is_spam,
is_question: is_question,
word_count: length(String.split(text, ~r/\s+/, trim: true)),
has_link: Regex.match?(~r/https?:\/\//, text),
language: detect_language(text)
}
end

defp analyze_sentiment(text) do
words = text |> String.downcase() |> String.split(~r/\W+/, trim: true)
pos = ~w(love great awesome amazing good excellent best wonderful fantastic helpful)
neg = ~w(hate bad terrible awful worst horrible poor ugly disappointing spam)
p = Enum.count(words, &(&1 in pos))
n = Enum.count(words, &(&1 in neg))
cond do
p > n -> :positive
n > p -> :negative
true -> :neutral
end
end

defp is_spam?(text) do
t = String.downcase(text)
spam_patterns = [~r/check my channel/, ~r/sub4sub/, ~r/free \w+ at/, ~r/click here/, ~r/bit\.ly/]
Enum.any?(spam_patterns, &Regex.match?(&1, t))
end

defp detect_language(text) do
cond do
Regex.match?(~r/[\p{Han}]/u, text) -> "zh"
Regex.match?(~r/[\p{Hiragana}\p{Katakana}]/u, text) -> "ja"
Regex.match?(~r/[\p{Hangul}]/u, text) -> "ko"
true -> "en"
end
end

defp build_response(analysis) do
cond do
analysis.is_spam -> %{action: :hide, text: nil}
analysis.is_question -> %{action: :reply, text: "Great question! Let me look into that."}
analysis.sentiment == :positive -> %{action: :heart, text: "Thank you so much! 🙏"}
analysis.sentiment == :negative -> %{action: :reply, text: "Sorry to hear that. Could you share more details so we can improve?"}
true -> %{action: :none, text: nil}
end
end

defp update_stats(stats, analysis) do
stats
|> Map.update!(:total, &(&1 + 1))
|> Map.update!(analysis.sentiment, &(&1 + 1))
|> then(fn s -> if analysis.is_spam, do: Map.update!(s, :spam, &(&1 + 1)), else: s end)
end
end
104 changes: 104 additions & 0 deletions lux/lib/lux/prisms/youtube/community/post_scheduler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
defmodule Lux.Prisms.YouTube.Community.PostScheduler do
@moduledoc """
Community post scheduling and campaign management.
"""

use GenServer

defstruct [:posts, :campaigns]

def start_link(opts \\ []) do
name = opts[:name] || __MODULE__
GenServer.start_link(__MODULE__, opts, name: name)
end

def schedule_post(pid \\ __MODULE__, post) do
GenServer.call(pid, {:schedule, post})
end

def cancel_post(pid \\ __MODULE__, post_id) do
GenServer.call(pid, {:cancel, post_id})
end

def get_due_posts(pid \\ __MODULE__) do
GenServer.call(pid, :get_due)
end

def create_campaign(pid \\ __MODULE__, campaign) do
GenServer.call(pid, {:create_campaign, campaign})
end

def list_posts(pid \\ __MODULE__) do
GenServer.call(pid, :list_posts)
end

def list_campaigns(pid \\ __MODULE__) do
GenServer.call(pid, :list_campaigns)
end

@impl true
def init(_opts), do: {:ok, %__MODULE__{posts: [], campaigns: []}}

@impl true
def handle_call({:schedule, post}, _from, state) do
id = post[:id] || gen_id()
entry = %{
id: id,
text: post[:text],
image_url: post[:image_url],
poll: post[:poll],
scheduled_at: post[:scheduled_at],
campaign_id: post[:campaign_id],
status: :scheduled,
platforms: post[:platforms] || [:youtube]
}
posts = [entry | state.posts] |> Enum.sort_by(& &1.scheduled_at, DateTime)
{:reply, {:ok, entry}, %{state | posts: posts}}
end

@impl true
def handle_call({:cancel, post_id}, _from, state) do
case Enum.find(state.posts, &(&1.id == post_id)) do
nil -> {:reply, {:error, :not_found}, state}
_ ->
posts = Enum.reject(state.posts, &(&1.id == post_id))
{:reply, :ok, %{state | posts: posts}}
end
end

@impl true
def handle_call(:get_due, _from, state) do
now = DateTime.utc_now()
{due, remaining} = Enum.split_with(state.posts, fn p ->
p.status == :scheduled && DateTime.compare(p.scheduled_at, now) in [:lt, :eq]
end)
{:reply, {:ok, due}, %{state | posts: remaining}}
end

@impl true
def handle_call({:create_campaign, campaign}, _from, state) do
id = campaign[:id] || gen_id()
entry = %{
id: id,
name: campaign[:name],
description: campaign[:description] || "",
start_date: campaign[:start_date],
end_date: campaign[:end_date],
status: :active,
created_at: DateTime.utc_now()
}
{:reply, {:ok, entry}, %{state | campaigns: [entry | state.campaigns]}}
end

@impl true
def handle_call(:list_posts, _from, state) do
{:reply, {:ok, state.posts}, state}
end

@impl true
def handle_call(:list_campaigns, _from, state) do
{:reply, {:ok, state.campaigns}, state}
end

defp gen_id, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
65 changes: 65 additions & 0 deletions lux/lib/lux/prisms/youtube/community/spam_detector.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule Lux.Prisms.YouTube.Community.SpamDetector do
@moduledoc """
Spam detection and moderation for YouTube comments.
"""

@spam_patterns [
{~r/sub4sub|sub 4 sub/i, 40},
{~r/check (out )?my channel/i, 35},
{~r/free \w+ at \w+\.\w+/i, 40},
{~r/click (the )?link/i, 30},
{~r/bit\.ly|tinyurl|t\.co/i, 25},
{~r/([\x{1F600}-\x{1F64F}].*){5,}/u, 15},
{~r/(.)\1{4,}/, 20},
{~r/https?:\/\/\S+/i, 10},
{~r/[A-Z\s]{20,}/, 15}
]

@doc "Detect spam in a comment. Returns score and matched patterns."
def detect(text) when is_binary(text) do
matches = @spam_patterns
|> Enum.filter(fn {pattern, _} -> Regex.match?(pattern, text) end)
|> Enum.map(fn {pattern, score} -> %{pattern: Regex.source(pattern), score: score} end)

total_score = Enum.sum(Enum.map(matches, & &1.score))

severity = cond do
total_score >= 60 -> :critical
total_score >= 40 -> :high
total_score >= 20 -> :medium
total_score > 0 -> :low
true -> :none
end

%{
is_spam: total_score >= 30,
score: total_score,
severity: severity,
matches: matches,
recommended_action: recommended_action(severity)
}
end

@doc "Batch detect spam."
def batch_detect(texts) when is_list(texts) do
results = Enum.map(texts, fn t ->
text = if is_binary(t), do: t, else: t[:text] || ""
{text, detect(text)}
end)

spam_count = Enum.count(results, fn {_, r} -> r.is_spam end)

%{
results: results,
total: length(results),
spam_count: spam_count,
spam_rate: if(length(results) > 0, do: Float.round(spam_count / length(results) * 100, 1), else: 0.0)
}
end

defp recommended_action(:critical), do: :ban_and_delete
defp recommended_action(:high), do: :delete
defp recommended_action(:medium), do: :hold_for_review
defp recommended_action(:low), do: :flag
defp recommended_action(:none), do: :allow
end
Loading