Skip to content

Commit 69c7534

Browse files
committed
Initial commit
0 parents  commit 69c7534

File tree

9 files changed

+257
-0
lines changed

9 files changed

+257
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
18+
19+
# Temporary files, for example, from tests.
20+
/tmp/
21+
22+
# Configuration file
23+
config.json

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Selaphiel Bot - Elixir version
2+
3+
The Messenger Archangel, now reimagined in Elixir. A **Discord-to-X relay bot** that listens for messages in specific Discord channels and posts them to X/Twitter. This is a reimplementation of the original [Selaphiel-bot (Python)](https://github.com/naghim/selaphiel-bot) done mostly because I wanted to try out Elixir! 💧✨
4+
5+
## How to run
6+
7+
1. Clone the repository
8+
9+
```bash
10+
git clone https://github.com/naghim/selaphiel_bot_elixir.git
11+
cd selaphiel_bot_elixir
12+
```
13+
14+
2. Install the dependencies
15+
Ensure you have Elixir and Erlang installed. Then:
16+
17+
```bash
18+
mix deps.get
19+
```
20+
21+
3. Set up configuration:
22+
Create a `config.json` file in the root directory (this is read at runtime by the app):
23+
24+
```json
25+
{
26+
"twitter_consumer_key": "your_twitter/X_key",
27+
"twitter_consumer_secret": "your_twitter/X_secret",
28+
"twitter_access_token": "your_access_token",
29+
"twitter_access_secret": "your_access_token_secret",
30+
"discord_token": "your_discord_bot_token",
31+
"discord_channel_ids": [111111111111111111]
32+
}
33+
```
34+
35+
> [!NOTE]
36+
> The reason for using the `config.json` file is to stay compatible with the original Python implementation, allowing both bots to be used interchangeably with the same configuration file.
37+
38+
4. Run the bot:
39+
40+
```bash
41+
mix run --no-halt
42+
```
43+
44+
## Dependencies
45+
46+
- [`nostrum`](https://hexdocs.pm/nostrum/) – Discord API client
47+
- [`extwitter`](https://hexdocs.pm/extwitter/) – Twitter API (v1.1)
48+
- [`jason`](https://hexdocs.pm/jason/) – JSON parser
49+
50+
## Limitations
51+
52+
While the original Python version uses the Twitter/X API v2, which allows posting tweets with fewer restrictions, this Elixir version relies on `ExTwitter`, which uses the older API v1.1. That API is now largely deprecated and requires a _paid developer account_ (Basic tier or higher) to post tweets. As a result, tweet posting may fail unless the required access level is granted.

config/config.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# config/config.exs
2+
import Config
3+
4+
# Enable debug logging
5+
config :logger,
6+
level: :debug,
7+
compile_time_purge_matching: [
8+
[level_lower_than: :info]
9+
]

lib/application.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
defmodule Selaphiel.Application do
3+
@moduledoc """
4+
Main application module that handles startup and configuration.
5+
"""
6+
7+
use Application
8+
require Logger
9+
10+
def start(_type, _args) do
11+
12+
settings = load_settings()
13+
14+
# Configure Nostrum
15+
Application.put_env(:nostrum, :auto_start, false)
16+
Application.put_env(:nostrum, :token, settings.discord_token)
17+
Application.put_env(:selaphiel, :discord_channel_ids, settings.discord_channel_ids)
18+
19+
# Configure ExTwitter
20+
ExTwitter.configure(
21+
consumer_key: settings.twitter_consumer_key,
22+
consumer_secret: settings.twitter_consumer_secret,
23+
access_token: settings.twitter_access_token,
24+
access_token_secret: settings.twitter_access_secret
25+
)
26+
27+
children = [{Selaphiel.NostrumManager, settings}, {Selaphiel.Consumer, []}]
28+
29+
opts = [strategy: :one_for_one, name: Selaphiel.Supervisor]
30+
Supervisor.start_link(children, opts)
31+
end
32+
33+
def load_settings do
34+
case File.read("config.json") do
35+
{:ok, content} ->
36+
Jason.decode!(content, keys: :atoms)
37+
{:error, reason} ->
38+
raise "Failed to load settings: #{reason}"
39+
end
40+
end
41+
end

lib/consumer.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule Selaphiel.Consumer do
2+
@moduledoc """
3+
Nostrum consumer that handles Discord events.
4+
"""
5+
6+
use Nostrum.Consumer
7+
@twitter_max_length 280
8+
9+
def handle_event({:READY, _data, _ws_state}) do
10+
IO.puts("Bot is ready!")
11+
{:ok, nil}
12+
end
13+
14+
def handle_event({:MESSAGE_CREATE, msg, _ws_state}) do
15+
ids = Application.get_env(:selaphiel, :discord_channel_ids, [])
16+
17+
if msg.channel_id in ids do
18+
content = "#{msg.content} (#{msg.author.global_name})"
19+
20+
split_tweet(content)
21+
|> Enum.each(fn chunk ->
22+
case ExTwitter.update(chunk) do
23+
{:ok, tweet} -> IO.inspect(tweet, label: "Tweet posted")
24+
{:error, reason} -> IO.puts("Failed to tweet: #{inspect(reason)}")
25+
end
26+
end)
27+
end
28+
29+
{:ok, nil}
30+
end
31+
32+
def handle_event(_event), do: {:ok, nil}
33+
34+
defp split_tweet(content) do
35+
content
36+
|> String.graphemes()
37+
|> Enum.chunk_every(@twitter_max_length)
38+
|> Enum.map(&Enum.join/1)
39+
end
40+
end

lib/nostrum_manager.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Selaphiel.NostrumManager do
2+
@moduledoc """
3+
Manages the Nostrum application lifecycle and configuration.
4+
This module ensures that Nostrum is properly configured and started
5+
before any events are processed.
6+
"""
7+
8+
use GenServer
9+
10+
def start_link(opts) do
11+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
12+
end
13+
14+
def init(_opts) do
15+
settings = Selaphiel.Application.load_settings()
16+
IO.puts("Loaded settings: #{inspect(settings)}")
17+
Application.put_env(:nostrum, :token, settings.discord_token)
18+
19+
# Manually start Nostrum's application
20+
{:ok, _} = Application.ensure_all_started(:nostrum)
21+
{:ok, %{}}
22+
end
23+
end

mix.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
defmodule DiscordTwitterBot.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :discord_twitter_bot,
7+
version: "0.1.0",
8+
elixir: "~> 1.18",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger],
18+
included_applications: [],
19+
mod: {Selaphiel.Application, []}
20+
]
21+
end
22+
23+
# Run "mix help deps" to learn about dependencies.
24+
defp deps do
25+
[
26+
# {:dep_from_hexpm, "~> 0.3.0"},
27+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
28+
{:nostrum, "~> 0.10.4", runtime: false},
29+
{:tesla, "~> 1.14.1"},
30+
{:jason, "~> 1.4.4"}, # For JSON parsing
31+
{:extwitter, "~> 0.14.0"},
32+
]
33+
end
34+
end

mix.lock

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
%{
2+
"castle": {:hex, :castle, "0.3.1", "e5d4f20696d878052a23c13158e1d372b24d9b30a6ea6f52fa6063c21c5ad67e", [:mix], [{:forecastle, "~> 0.1.3", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "3ee9ca04b069280ab4197fe753562958729c83b3aa08125255116a989e133835"},
3+
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
4+
"chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"},
5+
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
6+
"curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"},
7+
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
8+
"equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"},
9+
"extwitter": {:hex, :extwitter, "0.14.0", "5e15c5ea5e6a09baaf03fd5da6de1431eeeca0ef05a9c0f6cc62872e5b8816ed", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:oauther, "~> 1.3", [hex: :oauther, repo: "hexpm", optional: false]}], "hexpm", "5e9bbb491531317062df42ab56ccb8368addb00931b0785c8a5e1d652813b0a8"},
10+
"forecastle": {:hex, :forecastle, "0.1.3", "b07d217ef10799e6d6cc7e47407858e77b1a8cb248f15185534de3403de3aa42", [:mix], [], "hexpm", "07e1ffa79c56f3e0ead59f17c0163a747dafc210ca8f244a7e65a4bfa98dc96d"},
11+
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
12+
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
13+
"hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
14+
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
15+
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
16+
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
17+
"kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"},
18+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
19+
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
20+
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
21+
"nostrum": {:hex, :nostrum, "0.10.4", "a316d08b19104f34c5fd5aa56674907350899a5f0c2483afdf5586296bd0ce07", [:mix], [{:castle, "~> 0.3.0", [hex: :castle, repo: "hexpm", optional: false]}, {:certifi, "~> 2.13", [hex: :certifi, repo: "hexpm", optional: false]}, {:ezstd, "~> 1.1", [hex: :ezstd, repo: "hexpm", optional: true]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "fcc2642bf5b09792865ec2c26c1a11c6aa5432bc623a65dd81141e1eab9f1b99"},
22+
"oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"},
23+
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
24+
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
25+
"poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"},
26+
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"},
27+
"salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"},
28+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
29+
"tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"},
30+
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
31+
}

0 commit comments

Comments
 (0)