-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 66760e7
Showing
22 changed files
with
1,748 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# The directory Mix will write compiled artifacts to. | ||
/_build/ | ||
|
||
# If you run "mix test --cover", coverage assets end up here. | ||
/cover/ | ||
|
||
# The directory Mix downloads your dependencies sources to. | ||
/deps/ | ||
|
||
# Where third-party dependencies like ExDoc output generated docs. | ||
/doc/ | ||
|
||
# Ignore .fetch files in case you like to edit your project deps locally. | ||
/.fetch | ||
|
||
# If the VM crashes, it generates a dump, let's ignore it too. | ||
erl_crash.dump | ||
|
||
# Also ignore archive artifacts (built via "mix archive.build"). | ||
*.ez | ||
|
||
# Ignore package tarball (built via "mix hex.build"). | ||
qnotix-*.tar | ||
|
||
# Temporary files, for example, from tests. | ||
/tmp/ | ||
|
||
todo | ||
*.code-workspace | ||
JSON message* | ||
junk.ex | ||
qnotix.code* | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
## v1.0.1 | ||
* Initial release |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# Qnotix | ||
|
||
Qnotix is a minimalist Pub/Sub notification system written in Elixir based on just `Plug Cowboy` module and websockets. | ||
|
||
|
||
|
||
## Description | ||
|
||
Qnotix is a topic-based system, highly resilient, each topic running within its own, independent, supervised procees. | ||
|
||
The Pub side feeds events using HTML Post API. | ||
The Sub side is dispatching events through websocket connection. | ||
|
||
Both Pub and Sub sides depend and evolve on a named topic and its own port number. | ||
|
||
The format of messages, JSON, is similar to that of [ntfy](https://ntfy.sh/docs/subscribe/api/#json-message-format). | ||
As Sub client, the [ntfy Android app](https://ntfy.sh/docs/subscribe/phone/) must be used, the flavor available on [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), without Firebase. *Not being an Android developer, I would greatly appreciate support for building a dedicated Android client for Qnotix*. | ||
|
||
Find more details at [Qnotix documentation on Hexdocs](http://hexdocs.pm/qnotix). | ||
|
||
|
||
|
||
>This application (though slighly modified) is actually in production since March 2022 for a private surveillance company, serving more than 150 subscribers from 17 publishers. | ||
|
||
|
||
## Installation | ||
|
||
Add `qnotix` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:qnotix, "~> 1.0.0"} | ||
] | ||
end | ||
``` | ||
Set application's management port (backendPort) and start port numbering of topics (wsStartPort) in `runtime.exs`. | ||
|
||
|
||
|
||
## Usage | ||
|
||
|
||
|
||
|
||
Launch server: `iex -S mix` | ||
|
||
Register a new topic: `Qnotix.newTopic(topic_name)`. | ||
|
||
Launch a new topic on the desired port `Qnotix.newTopic(topic_name, port)`. | ||
|
||
Check the web management interface for supervising topics and ports, registering new topics, kill topics etc. | ||
|
||
By example, considering the server runnning localy on port 4000 and a topic named *myTopic* on port 4111 one can: | ||
- access web management interface: `http://localhost:4000` | ||
- register a new topic: `http://localhost:4000/new` | ||
- kill topic by name or port `http://localhost:4000/end` | ||
- publish notification to *myTopic* by POST method to `http://localhost:4111/pub` | ||
- web page to publish mock notifications to all subscribers for *myTopic* on `http://localhost:4111/` | ||
|
||
|
||
|
||
Qnotix is only compatible and working with [ntfy Android client app](https://ntfy.sh/docs/subscribe/phone/). The topic format/url is `ws://host:port/topic_name/ws`. Ex: `ws://192.168.1.1:4001/myTopic/ws` | ||
|
||
|
||
## Documentation | ||
http://hexdocs.pm/qnotix | ||
|
||
|
||
## TODO | ||
Kindly asking the Elixir community's support for: | ||
- development of a dedicated Android/IOS notification client for Qnotix | ||
- improved documentation | ||
- system extention for providing data streaming from 3rd party applications, services, or IoT devices (Nerves integration?) | ||
- scalability testing on distributed environmnent - multiple Erlang nodes, clustering | ||
- add security layer | ||
|
||
|
||
|
||
>As we lack expertise in mobile apps developmnet we would greatly appreciate the Community's involvement for development of a dedicated notification client for Android/IOS. | ||
|
||
## License | ||
Copyright © 2022 Quda Theo | ||
|
||
This software is released under **[AGPL-3.0-or-later](https://www.gnu.org/licenses/agpl-3.0.html)**. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Config | ||
|
||
config :qnotix, | ||
backendPort: 4000, | ||
wsStartPort: 4001, | ||
msgDeadAfter: 1 | ||
|
||
# se obtine cu: Application.get_env(:qnotix, :backendPort) | ||
# msgDeadAfter in days |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
defmodule Qnotix do | ||
require Logger | ||
alias Qnotix.TopicsManager | ||
|
||
@moduledoc """ | ||
Qnotix is a minimalist Pub/Sub notification system written in Elixir based on just `Plug Cowboy` module and websockets. | ||
## Description | ||
Qnotix is a topic-based system, highly resilient, each topic running within its own, independent, supervised procees. | ||
The Pub side feeds events using HTML Post API. | ||
The Sub side is dispatching events through websocket connection. | ||
Both Pub and Sub sides depend and evolve on a named topic and its own port number. | ||
The format of messages, JSON, is similar to that of [ntfy](https://ntfy.sh/docs/subscribe/api/#json-message-format). | ||
As Sub client, the [ntfy Android app](https://ntfy.sh/docs/subscribe/phone/) must be used, the flavor available on [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/) (no Firebase). *Not being an Android developer, I would greatly appreciate support for building a dedicated Android client for Qnotix*. | ||
""" | ||
|
||
################### | ||
# Qnotix main APIs | ||
################### | ||
|
||
@doc """ | ||
Launch a new topic. | ||
## Parameters | ||
- topic(string): the name of new topic | ||
## Examples | ||
``` | ||
iex(1)> Qnotix.newTopic("Hello") | ||
[notice] Topic Hello started on port 4001 | ||
{:ok, 4001} | ||
``` | ||
""" | ||
def newTopic(topic) when is_binary(topic) and byte_size(topic) > 0 do | ||
port = getFreePort(Application.get_env(:qnotix, :wsStartPort)) | ||
{flag, msg} = TopicsManager.start_child({topic, port}) | ||
|
||
case flag do | ||
:ok -> | ||
Logger.notice("Topic #{topic} started on port #{port}") | ||
{:ok, port} | ||
|
||
:error -> | ||
Logger.error("Topic #{topic} didn't start. Reason:") | ||
IO.inspect(msg) | ||
{:error, nil} | ||
end | ||
end | ||
|
||
@doc """ | ||
Launch a new topic on the desired port | ||
## Parameters | ||
- topic(string): the name of new topic | ||
- port(integer): port number | ||
## Examples | ||
``` | ||
iex(1)> Qnotix.newTopic("hello",4321) | ||
[notice] Topic hello started on port 4321 | ||
{:ok, 4321} | ||
``` | ||
""" | ||
@spec newTopic(binary, integer) :: {:error, nil} | {:ok, integer} | ||
def newTopic(topic, port) when is_binary(topic) and byte_size(topic) > 0 and is_integer(port) do | ||
{msg, _} = TopicsManager.start_child({topic, port}) | ||
|
||
case msg do | ||
:ok -> | ||
Logger.notice("Topic #{topic} started on port #{port}") | ||
{:ok, port} | ||
|
||
:error -> | ||
Logger.error("Topic #{topic} didn't start") | ||
{:error, nil} | ||
end | ||
end | ||
|
||
@doc """ | ||
Prints all running topics | ||
## Examples | ||
``` | ||
iex(1)> Qnotix.getTopics | ||
[ | ||
%{pid: "#PID<0.398.0>", port: 4321, topic: "hello"}, | ||
%{pid: "#PID<0.507.0>", port: 4001, topic: "kkt"} | ||
] | ||
``` | ||
""" | ||
@spec getTopics :: nil | [...] | ||
def getTopics do | ||
topics = | ||
Registry.select(:topics, [{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]) | ||
|> Enum.sort() | ||
|> Enum.map(fn {{topic, port}, pid} = _ -> | ||
%{topic: topic, port: port, pid: inspect(pid)} | ||
end) | ||
|
||
case topics do | ||
[_h | _t] -> topics | ||
[] -> nil | ||
end | ||
end | ||
|
||
@doc """ | ||
Returns the port for a certain topic | ||
## Parameters | ||
- topic(string): the name of new topic | ||
## Example | ||
``` | ||
iex(1)> Qnotix.getPortFromTopic("hello") | ||
4321 | ||
``` | ||
""" | ||
def getPortFromTopic(topic) do | ||
{{_, port}, _} = | ||
Registry.select(:topics, [{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]) | ||
|> Enum.find(fn {{regTopic, _port}, _pid} = _ -> regTopic == topic end) | ||
|
||
port | ||
end | ||
|
||
@doc """ | ||
Kills a topic by its name or by port | ||
## Parameters | ||
- topic(string): the name of new topic | ||
or | ||
- port(integer) | ||
## Example | ||
``` | ||
iex(3)> Qnotix.endTopic("hello") | ||
:ok | ||
``` | ||
""" | ||
@spec endTopic(binary | integer) :: | ||
:no_such_topic | :no_topic_on_port | :ok | {:error, :not_found} | ||
def endTopic(topic) when is_binary(topic) and byte_size(topic) > 0 do | ||
proc = | ||
Registry.select(:topics, [{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]) | ||
|> Enum.filter(fn {{regTopic, _port}, _pid} = _ -> regTopic == topic end) | ||
|
||
case proc do | ||
[{{_regTopic, _port}, pid}] -> DynamicSupervisor.terminate_child(TopicsManager, pid) | ||
_ -> :no_such_topic | ||
end | ||
end | ||
|
||
def endTopic(port) when is_integer(port) do | ||
proc = | ||
Registry.select(:topics, [{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]) | ||
|> Enum.filter(fn {{_regTopic, regPort}, _pid} = _ -> regPort == port end) | ||
|
||
case proc do | ||
[{{_regTopic, _port}, pid}] -> DynamicSupervisor.terminate_child(TopicsManager, pid) | ||
_ -> :no_topic_on_port | ||
end | ||
end | ||
|
||
##################### | ||
# Private functions | ||
##################### | ||
defp getFreePort(port) do | ||
case :gen_tcp.listen(port, [:binary]) do | ||
{:ok, socket} -> | ||
:ok = :gen_tcp.close(socket) | ||
port | ||
|
||
{:error, :eaddrinuse} -> | ||
getFreePort(port + 1) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
defmodule Qnotix.Application do | ||
@moduledoc false | ||
use Application | ||
|
||
@impl true | ||
def start(_type, _args) do | ||
children = [ | ||
{Registry, [keys: :unique, name: :topics]}, | ||
{Registry, [keys: :unique, name: :stores]}, | ||
{Plug.Cowboy, | ||
scheme: :http, | ||
plug: Qnotix.AppRouter, | ||
options: [port: Application.get_env(:qnotix, :backendPort)]}, | ||
{DynamicSupervisor, name: Qnotix.TopicsManager, strategy: :one_for_one} | ||
] | ||
|
||
opts = [strategy: :one_for_one, name: Qnotix.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
defmodule Qnotix.Utils do | ||
@moduledoc false | ||
def randomString(c \\ 20) do | ||
for _ <- 1..c, | ||
into: "", | ||
do: <<Enum.random('0123456789qwertyuiopasdfghjklzxcvbnmABCDEFGHIJKLMNOPQRSTUVWXYZ')>> | ||
end | ||
|
||
def timestamp do | ||
DateTime.utc_now() |> DateTime.to_unix() | ||
end | ||
|
||
def msgValability, do: Application.get_env(:qnotix, :msgDeadAfter) * 60 * 60 * 24 | ||
end |
Oops, something went wrong.