Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
huqedato committed Jul 17, 2022
0 parents commit 66760e7
Show file tree
Hide file tree
Showing 22 changed files with 1,748 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
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}"]
]
34 changes: 34 additions & 0 deletions .gitignore
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*


2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## v1.0.1
* Initial release
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions README.md
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)**.
9 changes: 9 additions & 0 deletions config/runtime.exs
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
186 changes: 186 additions & 0 deletions lib/qnotix.ex
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
20 changes: 20 additions & 0 deletions lib/qnotix/application.ex
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
14 changes: 14 additions & 0 deletions lib/qnotix/misc/utils.ex
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
Loading

0 comments on commit 66760e7

Please sign in to comment.