View Source Hello World

The purpose of this guide is to show you how to create a very basic server implementing the commands ping, register, login, whois, whoami, users and clients. We will be using ranch to provide a plain-text interface to our server.

Ranch is a tcp server and will allow us to provide an easy to test and prototype interface. For a more serious project you'll probably want to be looking at more structured data; for this example plain-text is fine.

Create project

mix new --sup hello_world_server

Add dependencies

In mix.exs

def deps do
  [
    {:teiserver, path: "../teiserver"},
    {:ecto_sql, "~> 3.10"},
    {:postgrex, ">= 0.0.0"},
    {:thousand_island, "~> 1.3"}
  ]
end

Now get the relevant dependencies

mix deps.get && mix compile

Create a migration

mix ecto.gen.migration add_teiserver_tables

And populate it like so:

defmodule HelloWorldServer.Repo.Migrations.AddTeiserverTables do
  use Ecto.Migration

  def up do
    Teiserver.Migration.up()
  end

  def down do
    Teiserver.Migration.down(version: 1)
  end
end

Application files

lib/hello_world_server/repo.ex

defmodule HelloWorldServer.Repo do
  use Ecto.Repo,
    otp_app: :hello_world_server,
    adapter: Ecto.Adapters.Postgres
end

config/config.exs

import Config

config :hello_world_server,
  ecto_repos: [HelloWorldServer.Repo]

config :hello_world_server, HelloWorldServer.Repo,
  database: "hello_world_server",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"

config :teiserver,
  repo: HelloWorldServer.Repo

Edit application.ex to start the various components on startup.

  children = [
    HelloWorldServer.Repo,
    {Ecto.Migrator,
        repos: Application.fetch_env!(:hello_world_server, :ecto_repos),
        skip: System.get_env("SKIP_MIGRATIONS") == "true"},

    {ThousandIsland, port: 8200, handler_module: HelloWorldServer.TcpServer}
  ]

Add our tcp server

Place in lib/hello_world_server/tcp_server.ex

defmodule HelloWorldServer.TcpServer do
  use ThousandIsland.Handler
  alias HelloWorldServer.{TcpIn, TcpOut}

  @impl ThousandIsland.Handler
  def handle_connection(socket, _state) do
    {:continue, %{
      user_id: nil,
      socket: socket
    }}
  end

  @impl ThousandIsland.Handler
  # If Ctrl + C is sent through it kills the connection, makes telnet debugging easier
  def handle_data(<<255, 244, 255, 253, 6>>, _socket, state) do
    {:close, state}
  end

  def handle_data(data, socket, state) do
    {new_state, response} = TcpIn.data_in(String.trim(data), state)
    TcpOut.data_out(response, new_state)
    {:continue, new_state}
  end
end

Data in

Place in lib/hellow_world_server/tcp_in.ex, this will handle all the commands coming in.

defmodule HelloWorldServer.TcpIn do
  alias Teiserver.Api

  def data_in("ping" <> _data, state) do
    {state, "pong"}
  end

  def data_in("login " <> data, state) do
    [name, password] = String.split(data, " ")
    case Api.maybe_authenticate_user(name, password) do
      {:ok, user} ->
        Api.connect_user(user)
        {%{state | user_id: user.id}, "You are now logged in as '#{user.name}'"}
      {:error, :no_user} ->
        {state, "Login failed (no user)"}
      {:error, :bad_password} ->
        {state, "Login failed (bad password)"}
    end
  end

  def data_in("register " <> data, state) do
    [name, password] = String.split(data, " ")
    
    # We're not using emails right now but Teiserver expects them to be unique
    # this will do for the purposes of this example
    email = to_string(:rand.uniform())
    
    case Api.register_user(name, email, password) do
      {:ok, _user} ->
        {state, "User created, you can now login with 'login name password'"}
      {:error, _} ->
        {state, "Error registering user"}
    end
  end

  def data_in("whois " <> data, state) do
    case Teiserver.Account.get_user_by_name(data) do
      nil ->
        {state, "I cannot find a user by the name of '#{data}'"}
      user ->
        {state, "User #{user.name} exists with an ID of #{user.id}"}
    end
  end

  def data_in("whoami" <> _data, %{user_id: user_id} = state) do
    case Teiserver.Account.get_user_by_id(user_id) do
      nil ->
        {state, "You are not logged in"}
      user ->
        {state, "You are '#{user.name}'"}
    end
  end

  def data_in("users" <> _data, state) do
    names = Teiserver.Account.list_users(select: [:name])
    |> Enum.map(fn %{name: name} -> name end)
    |> Enum.join(", ")

    {state, "User names: #{names}"}
  end

  def data_in("clients" <> _data, state) do
    client_ids = Teiserver.Connections.list_client_ids()

    names = Teiserver.Account.list_users(where: [id_in: client_ids], select: [:name])
    |> Enum.map(fn %{name: name} -> name end)
    |> Enum.join(", ")

    {state, "Client names: #{names}"}
  end
end

Data out

Place in lib/hellow_world_server/tcp_out.ex, this will handle sending data back to our users.

defmodule HelloWorldServer.TcpOut do
  def data_out(msg, state) do
    ThousandIsland.Socket.send(state.socket, "#{msg}\n")
  end
end

Showtime!

Get deps We now need to run our application.

iex -S mix run --no-halt

This will open up a REPL into the application and we can run queries manually if we want to such as:

Teisever.Account.list_users()
Teisever.Account.list_clients()

Telnet

In another terminal we can then do:

telnet localhost 8200
# Trying 127.0.0.1...
# Connected to localhost.
# Escape character is '^]'.

ping
# pong

whoami
# You are not logged in

whois teifion
# I cannot find a user by the name of 'Teifion'

register teifion password1
# User created, you can now login with 'login name password'

whois teifion
# User Teifion exists with an ID of 1

login teifion nopass
# Login failed (bad password)

login teifion password1
# You are now logged in as Teifion

whoami
# You are 'Teifion'

register bob password1
# User created, you can now login with 'login name password'

users
# User names: teifion, bob

clients
# Client names: teifion