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
73 changes: 73 additions & 0 deletions lux/guides/discord_core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Discord Core Prisms Guide

Core Discord server operations: messages, channels, moderation, events.

## Message Management

```elixir
# Send
{:ok, msg} = MessageManagement.send_message("channel_id", %{content: "Hello!", reply_to: "msg_id"}, opts)

# Edit / Delete
MessageManagement.edit_message("ch", "msg_id", %{content: "Updated"}, opts)
MessageManagement.delete_message("ch", "msg_id", opts)

# Bulk delete (2-100 messages)
MessageManagement.bulk_delete("ch", ["m1", "m2", "m3"], opts)

# History, Pin, React
{:ok, messages} = MessageManagement.get_history("ch", %{limit: 50}, opts)
MessageManagement.pin_message("ch", "msg_id", opts)
MessageManagement.add_reaction("ch", "msg_id", "👍", opts)
```

## Channel Management

```elixir
# CRUD
{:ok, ch} = ChannelManagement.create_channel("guild_id", %{name: "general", type: :text}, opts)
ChannelManagement.edit_channel("ch_id", %{name: "renamed", topic: "New topic"}, opts)
ChannelManagement.delete_channel("ch_id", opts)

# Permissions
ChannelManagement.set_permission("ch_id", "role_id", %{type: 0, allow: 1024, deny: 0}, opts)

# Archive/Unarchive
ChannelManagement.archive_channel("ch_id", opts)
```

## Moderation

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

# Content filtering (built-in: spam links, all caps)
{:ok, result} = Moderation.check_content(pid, "Join discord.gg/spam!")
# %{clean: false, violations: [%{action: :delete, severity: :high}]}

Moderation.add_filter(pid, %{pattern: "badword", action: :ban, severity: :critical})
Moderation.add_filter(pid, %{pattern: ~r/regex/i, action: :delete})

# Warnings
Moderation.warn_user(pid, "user_id", "Spamming")
{:ok, warnings} = Moderation.get_warnings(pid, "user_id")

# Discord API actions
Moderation.timeout_user("guild", "user", 3600, opts) # 1h timeout
Moderation.ban_user("guild", "user", %{delete_message_seconds: 86400}, opts)
Moderation.kick_user("guild", "user", opts)
```

## Event Handling

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

{:ok, event} = EventHandling.create_event(pid, %{
name: "Game Night", start_time: ~U[2026-03-15 20:00:00Z], channel_id: "ch1"
})

EventHandling.add_reminder(pid, event.id, 30) # 30 min before
EventHandling.rsvp(pid, event.id, "user1", :going)
{:ok, due} = EventHandling.get_due_reminders(pid)
```
76 changes: 76 additions & 0 deletions lux/lib/lux/prisms/discord/channel_management.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule Lux.Prisms.Discord.ChannelManagement do
@moduledoc """
Channel management prism: CRUD, permissions, archiving.
"""

alias Lux.Integrations.Discord.Client

@channel_types %{text: 0, voice: 2, category: 4, announcement: 5, forum: 15, stage: 13}

@doc "Create a channel in a guild."
def create_channel(guild_id, params, opts \\ %{}) do
body = %{
name: params[:name],
type: Map.get(@channel_types, params[:type] || :text, 0)
}
|> maybe_put(:topic, params[:topic])
|> maybe_put(:parent_id, params[:parent_id])
|> maybe_put(:position, params[:position])
|> maybe_put(:nsfw, params[:nsfw])
|> maybe_put(:rate_limit_per_user, params[:slowmode])

Client.request(:post, "/guilds/#{guild_id}/channels", Map.merge(opts, %{json: body}))
end

@doc "Edit a channel."
def edit_channel(channel_id, params, opts \\ %{}) do
body = %{}
|> maybe_put(:name, params[:name])
|> maybe_put(:topic, params[:topic])
|> maybe_put(:position, params[:position])
|> maybe_put(:nsfw, params[:nsfw])
|> maybe_put(:rate_limit_per_user, params[:slowmode])
|> maybe_put(:parent_id, params[:parent_id])

Client.request(:patch, "/channels/#{channel_id}", Map.merge(opts, %{json: body}))
end

@doc "Delete a channel."
def delete_channel(channel_id, opts \\ %{}) do
Client.request(:delete, "/channels/#{channel_id}", opts)
end

@doc "Get channel info."
def get_channel(channel_id, opts \\ %{}) do
Client.request(:get, "/channels/#{channel_id}", opts)
end

@doc "Set permission overwrite for a channel."
def set_permission(channel_id, overwrite_id, params, opts \\ %{}) do
body = %{
id: overwrite_id,
type: params[:type] || 0,
allow: to_string(params[:allow] || 0),
deny: to_string(params[:deny] || 0)
}
Client.request(:put, "/channels/#{channel_id}/permissions/#{overwrite_id}", Map.merge(opts, %{json: body}))
end

@doc "Archive a channel (set archived flag for threads/forums)."
def archive_channel(channel_id, opts \\ %{}) do
Client.request(:patch, "/channels/#{channel_id}", Map.merge(opts, %{json: %{archived: true}}))
end

@doc "Unarchive a channel."
def unarchive_channel(channel_id, opts \\ %{}) do
Client.request(:patch, "/channels/#{channel_id}", Map.merge(opts, %{json: %{archived: false}}))
end

@doc "List guild channels."
def list_channels(guild_id, opts \\ %{}) do
Client.request(:get, "/guilds/#{guild_id}/channels", opts)
end

defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end
124 changes: 124 additions & 0 deletions lux/lib/lux/prisms/discord/event_handling.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
defmodule Lux.Prisms.Discord.EventHandling do
@moduledoc """
Event scheduling, reminders, and notification management for Discord.
"""

use GenServer

defstruct [:events, :reminders, :notifications]

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

def create_event(pid \\ __MODULE__, params) do
GenServer.call(pid, {:create_event, params})
end

def cancel_event(pid \\ __MODULE__, event_id) do
GenServer.call(pid, {:cancel_event, event_id})
end

def list_events(pid \\ __MODULE__, opts \\ %{}) do
GenServer.call(pid, {:list_events, opts})
end

def get_event(pid \\ __MODULE__, event_id) do
GenServer.call(pid, {:get_event, event_id})
end

def add_reminder(pid \\ __MODULE__, event_id, minutes_before) do
GenServer.call(pid, {:add_reminder, event_id, minutes_before})
end

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

def rsvp(pid \\ __MODULE__, event_id, user_id, status) do
GenServer.call(pid, {:rsvp, event_id, user_id, status})
end

@impl true
def init(_opts) do
{:ok, %__MODULE__{events: %{}, reminders: [], notifications: []}}
end

@impl true
def handle_call({:create_event, params}, _from, state) do
id = params[:id] || gen_id()
event = %{
id: id,
name: params[:name],
description: params[:description] || "",
channel_id: params[:channel_id],
guild_id: params[:guild_id],
start_time: params[:start_time],
end_time: params[:end_time],
rsvps: %{},
status: :scheduled,
created_at: DateTime.utc_now()
}
{:reply, {:ok, event}, %{state | events: Map.put(state.events, id, event)}}
end

@impl true
def handle_call({:cancel_event, event_id}, _from, state) do
case Map.get(state.events, event_id) do
nil -> {:reply, {:error, :not_found}, state}
event ->
updated = %{event | status: :cancelled}
{:reply, :ok, %{state | events: Map.put(state.events, event_id, updated)}}
end
end

@impl true
def handle_call({:list_events, opts}, _from, state) do
events = Map.values(state.events)
events = if opts[:status], do: Enum.filter(events, &(&1.status == opts[:status])), else: events
{:reply, {:ok, Enum.sort_by(events, & &1.start_time, DateTime)}, state}
end

@impl true
def handle_call({:get_event, event_id}, _from, state) do
case Map.get(state.events, event_id) do
nil -> {:reply, {:error, :not_found}, state}
event -> {:reply, {:ok, event}, state}
end
end

@impl true
def handle_call({:add_reminder, event_id, minutes_before}, _from, state) do
case Map.get(state.events, event_id) do
nil -> {:reply, {:error, :not_found}, state}
event ->
remind_at = DateTime.add(event.start_time, -minutes_before * 60)
reminder = %{event_id: event_id, remind_at: remind_at, sent: false}
{:reply, {:ok, reminder}, %{state | reminders: [reminder | state.reminders]}}
end
end

@impl true
def handle_call(:get_due_reminders, _from, state) do
now = DateTime.utc_now()
{due, remaining} = Enum.split_with(state.reminders, fn r ->
!r.sent && DateTime.compare(r.remind_at, now) in [:lt, :eq]
end)
marked = Enum.map(due, &%{&1 | sent: true})
{:reply, {:ok, due}, %{state | reminders: marked ++ remaining}}
end

@impl true
def handle_call({:rsvp, event_id, user_id, status}, _from, state) do
case Map.get(state.events, event_id) do
nil -> {:reply, {:error, :not_found}, state}
event ->
updated = %{event | rsvps: Map.put(event.rsvps, user_id, status)}
{:reply, {:ok, %{user_id: user_id, status: status}},
%{state | events: Map.put(state.events, event_id, updated)}}
end
end

defp gen_id, do: :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower)
end
91 changes: 91 additions & 0 deletions lux/lib/lux/prisms/discord/message_management.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Lux.Prisms.Discord.MessageManagement do
@moduledoc """
Message management prism for Discord: create, edit, delete, bulk delete, history.
Includes rate limit tracking and retry mechanisms.
"""

alias Lux.Integrations.Discord.Client

@max_bulk_delete 100
@max_retries 3

@doc "Send a message to a channel."
def send_message(channel_id, params, opts \\ %{}) do
body = %{content: params[:content]}
|> maybe_put(:embeds, params[:embeds])
|> maybe_put(:components, params[:components])
|> maybe_put(:message_reference, if(params[:reply_to], do: %{message_id: params[:reply_to]}))

with_retry(fn ->
Client.request(:post, "/channels/#{channel_id}/messages", Map.merge(opts, %{json: body}))
end)
end

@doc "Edit an existing message."
def edit_message(channel_id, message_id, params, opts \\ %{}) do
body = %{}
|> maybe_put(:content, params[:content])
|> maybe_put(:embeds, params[:embeds])

with_retry(fn ->
Client.request(:patch, "/channels/#{channel_id}/messages/#{message_id}", Map.merge(opts, %{json: body}))
end)
end

@doc "Delete a message."
def delete_message(channel_id, message_id, opts \\ %{}) do
with_retry(fn ->
Client.request(:delete, "/channels/#{channel_id}/messages/#{message_id}", opts)
end)
end

@doc "Bulk delete messages (2-100 messages, not older than 14 days)."
def bulk_delete(channel_id, message_ids, opts \\ %{}) when length(message_ids) <= @max_bulk_delete do
with_retry(fn ->
Client.request(:post, "/channels/#{channel_id}/messages/bulk-delete",
Map.merge(opts, %{json: %{messages: message_ids}}))
end)
end

@doc "Get message history for a channel."
def get_history(channel_id, params \\ %{}, opts \\ %{}) do
query = %{}
|> maybe_put(:limit, params[:limit] || 50)
|> maybe_put(:before, params[:before])
|> maybe_put(:after, params[:after])
|> maybe_put(:around, params[:around])

query_string = query |> Enum.reject(fn {_, v} -> is_nil(v) end) |> URI.encode_query()
path = "/channels/#{channel_id}/messages?#{query_string}"

with_retry(fn -> Client.request(:get, path, opts) end)
end

@doc "Pin a message."
def pin_message(channel_id, message_id, opts \\ %{}) do
with_retry(fn ->
Client.request(:put, "/channels/#{channel_id}/pins/#{message_id}", opts)
end)
end

@doc "Add a reaction to a message."
def add_reaction(channel_id, message_id, emoji, opts \\ %{}) do
encoded = URI.encode(emoji)
with_retry(fn ->
Client.request(:put, "/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded}/@me", opts)
end)
end

defp with_retry(fun, attempt \\ 1) do
case fun.() do
{:error, %{status: 429} = resp} when attempt < @max_retries ->
retry_after = get_in(resp, [:body, "retry_after"]) || 1
Process.sleep(trunc(retry_after * 1000))
with_retry(fun, attempt + 1)
result -> result
end
end

defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end
Loading