diff --git a/README.md b/README.md index 420727b..7f7d72c 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,73 @@ # KolabChat *WARNING: PRE-ALPHA SOFTWARE, WILL EAT ALL THE BABIES* KolabChat provides an Instant Messaging and Video Chat application. The goal of this project is to give Kolab users tools to communicate with each other in real time using web browser. To learn about Kolab visit https://kolab.org # Building This covers building for development purposes: * Install dependencies with `mix deps.get` * Optional steps * Install inotify-tools (or equiv) package for live reloading during devel * Create and migrate your database with `mix ecto.create && mix ecto.migrate` * Node.js components * Node.js is only a build dependency, and is used to build the web assets For just running Kolab Chat, Node.js is NOT required. * Node.js version 5.0.0 or newer is required. * Install dependencies with `npm install` * Install babel presets: `npm install babel-preset-es2015 babel-preset-es2016` +* Initialize database: `mix amnesia.create -db KolabChat.Database --disk` * Start Phoenix endpoint with `mix phoenix.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. Milestone I. ------------ - authenticate against Kolab (Basic http auth) - think about generic component for Kolab auth - Aaron should look for it on his laptop ;) - setting user presence status and receiving other users status - notifications - person-to-person chats (https://github.com/chrismccord/phoenix_chat_example) - single page web application that shows contacts list and you can start chats - How to start a chat, set presence, display presence? - How to define/start a group chat? invitations? - "Chat window" layout - groups chats - webRTC (https://hashrocket.com/blog/posts/implementing-video-chat-in-a-phoenix-application-with-webrtc) Milestone II. ------------- - Roundcube integration - Current user presence indicator in the toolbar, with possibility to set status - A way to start a chat by clicking a user name/email address/status icon in the email view and addressbook - Other users status on contacts list - How to start a chat from UI? - How to show the chat "window"? - Kube integration - Jabber bridge (internal organization communication) - UI customization Milestone III. -------------- - File transfer - End-to-end Encryption - Multi-user video chats - Federation (communication between Kolab servers) - Server-side chat history/state - Single-use invitations for external people - Chat-room "booking" and integration with calendar Tools: ------ - Plug, Phoenix's pubsub (https://github.com/phoenixframework/phoenix_pubsub) - Javascript client for pubsub - ueberauth? diff --git a/config/config.exs b/config/config.exs index 65de43d..53e09d0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,29 +1,28 @@ # This file is responsible for configuring your application # and its dependencies with the aid of the Mix.Config module. # # This configuration file is loaded before any dependency and # is restricted to this project. use Mix.Config # General application configuration config :kolab_chat, - ecto_repos: [KolabChat.Repo], salts: [session_signing: "M7HpCp6W", session_encryption: nil] # Configures the endpoint config :kolab_chat, KolabChat.Endpoint, url: [host: "localhost"], secret_key_base: "XCVqlNuOTjBK3GB4lPKKdoTk9149ftPIJmytpQnYxI4qpGwjJbR47bYdzOAggBii", render_errors: [view: KolabChat.ErrorView, accepts: ~w(html json)], pubsub: [name: KolabChat.PubSub, adapter: Phoenix.PubSub.PG2] # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env}.exs" diff --git a/config/dev.exs b/config/dev.exs index 88d7498..b4886aa 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,43 +1,34 @@ use Mix.Config # For development, we disable any cache and enable # debugging and code reloading. # # The watchers configuration can be used to run external # watchers to your application. For example, we use it # with brunch.io to recompile .js and .css sources. config :kolab_chat, KolabChat.Endpoint, http: [port: 4000], debug_errors: true, code_reloader: true, check_origin: false, watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", cd: Path.expand("../", __DIR__)]] # Watch static and templates for browser reloading. config :kolab_chat, KolabChat.Endpoint, live_reload: [ patterns: [ ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, ~r{priv/gettext/.*(po)$}, ~r{web/views/.*(ex)$}, ~r{web/templates/.*(eex)$} ] ] # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. config :phoenix, :stacktrace_depth, 20 - -# Configure your database -config :kolab_chat, KolabChat.Repo, - adapter: Ecto.Adapters.MySQL, - username: "root", - password: "12345", - database: "kolab_chat_dev", - hostname: "localhost", - pool_size: 10 diff --git a/config/test.exs b/config/test.exs index 1b65ffa..4172c66 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,19 +1,10 @@ use Mix.Config # We don't run a server during test. If one is required, # you can enable the server option below. config :kolab_chat, KolabChat.Endpoint, http: [port: 4001], server: false # Print only warnings and errors during test config :logger, level: :warn - -# Configure your database -config :kolab_chat, KolabChat.Repo, - adapter: Ecto.Adapters.MySQL, - username: "root", - password: "12345", - database: "kolab_chat_test", - hostname: "localhost", - pool: Ecto.Adapters.SQL.Sandbox diff --git a/lib/kolab_chat.ex b/lib/kolab_chat.ex index 8259186..69aff4d 100644 --- a/lib/kolab_chat.ex +++ b/lib/kolab_chat.ex @@ -1,33 +1,31 @@ defmodule KolabChat do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec # Define workers and child supervisors to be supervised children = [ - # Start the Ecto repository - supervisor(KolabChat.Repo, []), # Start the endpoint when the application starts supervisor(KolabChat.Endpoint, []), # Start phoenix presence module supervisor(KolabChat.Presence, []), # Start your own worker by calling: KolabChat.Worker.start_link(arg1, arg2, arg3) # worker(KolabChat.Worker, [arg1, arg2, arg3]), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: KolabChat.Supervisor] Supervisor.start_link(children, opts) end # Tell Phoenix to update the endpoint configuration # whenever the application is updated. def config_change(changed, _new, removed) do KolabChat.Endpoint.config_change(changed, removed) :ok end end diff --git a/lib/kolab_chat/database.ex b/lib/kolab_chat/database.ex new file mode 100644 index 0000000..bd93327 --- /dev/null +++ b/lib/kolab_chat/database.ex @@ -0,0 +1,19 @@ +use Amnesia + +defdatabase KolabChat.Database do + + deftable User, [{:id, autoincrement}, :username, :fullname], type: :ordered_set, index: [:username] do + + # Find a user by username + def find(username) do + Amnesia.transaction do + case User.read_at(username, :username) do + nil -> nil + users -> Enum.at(users, 0) + end + end + end + + end + +end diff --git a/lib/kolab_chat/repo.ex b/lib/kolab_chat/repo.ex deleted file mode 100644 index 25b4387..0000000 --- a/lib/kolab_chat/repo.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule KolabChat.Repo do - use Ecto.Repo, otp_app: :kolab_chat -end diff --git a/mix.exs b/mix.exs index c06a57a..f43a105 100644 --- a/mix.exs +++ b/mix.exs @@ -1,54 +1,51 @@ defmodule KolabChat.Mixfile do use Mix.Project def project do [app: :kolab_chat, version: "0.0.1", elixir: "~> 1.2", elixirc_paths: elixirc_paths(Mix.env), compilers: [:phoenix, :gettext] ++ Mix.compilers, build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, aliases: aliases(), deps: deps()] end # Configuration for the OTP application. # # Type `mix help compile.app` for more information. def application do [mod: {KolabChat, []}, applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, - :phoenix_ecto, :mariaex]] + :amnesia]] end # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "web", "test/support"] defp elixirc_paths(_), do: ["lib", "web"] # Specifies your project dependencies. # # Type `mix help deps` for examples and options. defp deps do [{:phoenix, "~> 1.2.1"}, {:phoenix_pubsub, "~> 1.0"}, - {:phoenix_ecto, "~> 3.0"}, - {:mariaex, "~> 0.7.9", override: true}, {:phoenix_html, "~> 2.6"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.11"}, + {:amnesia, "~> 0.2.0"}, {:cowboy, "~> 1.0"}] end # Aliases are shortcuts or tasks specific to the current project. # For example, to create, migrate and run the seeds file at once: # # $ mix ecto.setup # # See the documentation for `Mix` for more info on aliases. defp aliases do - ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"], - "test": ["ecto.create --quiet", "ecto.migrate", "test"]] + [] end end diff --git a/mix.lock b/mix.lock index f01dbbd..752e094 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,14 @@ -%{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, +%{"amnesia": {:hex, :amnesia, "0.2.5", "3202e0b01e380671274caea32fbe78bbd1989e1215215f41a2d97583e5c7d163", [:mix], [{:exquisite, "~> 0.1.6", [hex: :exquisite, optional: false]}]}, "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, - "db_connection": {:hex, :db_connection, "1.0.0", "63c03e520d54886a66104d34e32397ba960db6e74b596ce221592c07d6a40d8d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, - "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, - "ecto": {:hex, :ecto, "2.0.6", "9dcbf819c2a77f67a66b83739b7fcc00b71aaf6c100016db4f798930fa4cfd47", [:mix], [{:db_connection, "~> 1.0", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, + "exquisite": {:hex, :exquisite, "0.1.7", "4106503e976f409246731b168cd76eb54262bd04f4facc5cba82c2f53982aaf0", [:mix], []}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, "gettext": {:hex, :gettext, "0.13.0", "daafbddc5cda12738bb93b01d84105fe75b916a302f1c50ab9fb066b95ec9db4", [:mix], []}, - "mariaex": {:hex, :mariaex, "0.7.9", "52f837cf1b0717f95a0e62624bb99707329cba599885cf3bd2fdecc0172a98ad", [:mix], [{:db_connection, "~> 1.0.0-rc", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, - "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, - "phoenix_html": {:hex, :phoenix_html, "2.8.0", "777598a4b6609ad6ab8b180f7b25c9af2904644e488922bb9b9b03ce988d20b1", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, + "phoenix_html": {:hex, :phoenix_html, "2.9.2", "371160b30cf4e10443b015efce6f03e1f19aae98ff6487620477b13a5b2ef660", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.6", "4490d588c4f60248b1c5f1f0dc0a7271e1aed4bddbd8b1542630f7bf6bc7b012", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, - "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []}} diff --git a/web/channels/user_socket.ex b/web/channels/user_socket.ex index 980d51c..ca20994 100644 --- a/web/channels/user_socket.ex +++ b/web/channels/user_socket.ex @@ -1,42 +1,41 @@ defmodule KolabChat.UserSocket do use Phoenix.Socket - alias KolabChat.Repo - alias KolabChat.User + alias KolabChat.Database ## Channels channel "room:*", KolabChat.RoomChannel channel "system", KolabChat.SystemChannel ## Transports transport :websocket, Phoenix.Transports.WebSocket transport :longpoll, Phoenix.Transports.LongPoll # Socket params are passed from the client and can # be used to verify and authenticate a user. After # verification, you can put default assigns into # the socket that will be set for all channels, ie # {:ok, assign(socket, :user_id, verified_user_id)} # To deny connection, return `:error`. def connect(%{"token" => token}, socket) do case Phoenix.Token.verify(socket, "user", token, max_age: 86400) do {:ok, user_id} -> - socket = assign(socket, :user, Repo.get!(User, user_id)) + socket = assign(socket, :user, Database.User.read!(user_id)) {:ok, socket} {:error, _} -> :error end end # Socket id's are topics that allow you to identify all sockets for a given user: # # def id(socket), do: "users_socket:#{socket.assigns.user_id}" # # Would allow you to broadcast a "disconnect" event and terminate # all active sockets and channels for a given user: # # KolabChat.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) # # Returning `nil` makes this socket anonymous. def id(_socket), do: nil end diff --git a/web/controllers/auth_controller.ex b/web/controllers/auth_controller.ex index c031811..d1c347c 100644 --- a/web/controllers/auth_controller.ex +++ b/web/controllers/auth_controller.ex @@ -1,52 +1,59 @@ defmodule KolabChat.AuthController do use KolabChat.Web, :controller + # FIXME: is there a better place to put these + # Both are required for using Amnesia.transaction + require Amnesia + require Amnesia.Helper + @doc """ Handler for the default logon form """ def default_callback(conn, params) do %{"logon" => %{"password" => _pass, "username" => user}} = params cond do is_nil(user) or user == "" -> conn |> put_flash(:error, gettext("Invalid username!")) |> redirect(to: "/") true -> - changeset = User.changeset(%User{}, %{username: user}) - signin(conn, changeset) + signin(conn, user) end end - defp signin(conn, changeset) do - case insert_or_update_user(changeset) do + defp signin(conn, username) do + case insert_or_update_user(username) do {:ok, user} -> conn |> put_flash(:info, gettext("Signed in!")) |> put_session(:user_id, user.id) |> redirect(to: "/") {:error, _reason} -> conn |> put_flash(:error, gettext("Error signing in")) |> redirect(to: "/") end end - defp insert_or_update_user(changeset) do - case Repo.get_by(User, username: changeset.changes.username) do + defp insert_or_update_user(username) do + case Database.User.find(username) do nil -> - Repo.insert(changeset) + user = Amnesia.transaction do + Database.User.write!(%Database.User{username: username}) + end + {:ok, user} user -> {:ok, user} end end @doc """ Handler for logout action """ def logout(conn, _params) do conn |> configure_session(drop: true) |> redirect(to: "/") end end diff --git a/web/controllers/plugs/set_user.ex b/web/controllers/plugs/set_user.ex index 8d77a68..9075daa 100644 --- a/web/controllers/plugs/set_user.ex +++ b/web/controllers/plugs/set_user.ex @@ -1,30 +1,36 @@ defmodule KolabChat.Plugs.SetUser do import Plug.Conn - alias KolabChat.Repo - alias KolabChat.User + use KolabChat.Database def init(params), do: params # token authentication def call(%{"params": %{"token" => token}} = conn, _params) do case Phoenix.Token.verify(conn, "user", token, max_age: 86400) do {:ok, user_id} -> - assign(conn, :user, Repo.get!(User, user_id)) + assign(conn, :user, get_user_by_id(user_id)) _ -> assign(conn, :user, nil) end end # session authentication def call(conn, _params) do user_id = get_session(conn, :user_id) cond do - user = user_id && Repo.get(User, user_id) -> + user = user_id && get_user_by_id(user_id) -> assign(conn, :user, user) true -> assign(conn, :user, nil) end end + + def get_user_by_id(user_id) do + require Amnesia.Helper # FIXME: this is required by Amnesia.transaction, where to put it best? + Amnesia.transaction do + Database.User.read(user_id) + end + end end diff --git a/web/models/user.ex b/web/models/user.ex deleted file mode 100644 index 40ab9a0..0000000 --- a/web/models/user.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule KolabChat.User do - use KolabChat.Web, :model - - schema "users" do - field :username, :string - end - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:username]) - |> validate_required([:username]) - end -end diff --git a/web/web.ex b/web/web.ex index 4a96a53..2f9e7d8 100644 --- a/web/web.ex +++ b/web/web.ex @@ -1,83 +1,66 @@ defmodule KolabChat.Web do @moduledoc """ A module that keeps using definitions for controllers, views and so on. This can be used in your application as: use KolabChat.Web, :controller use KolabChat.Web, :view The definitions below will be executed for every view, controller, etc, so keep them short and clean, focused on imports, uses and aliases. Do NOT define functions inside the quoted expressions below. """ - def model do - quote do - use Ecto.Schema - - import Ecto - import Ecto.Changeset - import Ecto.Query - end - end - def controller do quote do use Phoenix.Controller - alias KolabChat.Repo - alias KolabChat.User - - import Ecto - import Ecto.Query + alias KolabChat.Database import KolabChat.Router.Helpers import KolabChat.Gettext end end def view do quote do use Phoenix.View, root: "web/templates" # Import convenience functions from controllers import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML import KolabChat.Router.Helpers import KolabChat.ErrorHelpers import KolabChat.Gettext end end def router do quote do use Phoenix.Router end end def channel do quote do use Phoenix.Channel - alias KolabChat.Repo - import Ecto - import Ecto.Query import KolabChat.Gettext end end @doc """ When used, dispatch to the appropriate controller/view/etc. """ defmacro __using__(which) when is_atom(which) do apply(__MODULE__, which, []) end end