├── vsn1
├── README.md
├── ChannelState.cs
├── packages.config
├── ChannelEvents.cs
├── RecHook.cs
├── AfterHook.cs
├── Envelope.cs
├── CSharpPhoenixClient.sln
├── PhoenixClient.sln
├── Properties
│ └── AssemblyInfo.cs
├── PhoenixClient.csproj
├── Push.cs
├── Channel.cs
└── Socket.cs
├── sample_server
├── test
│ ├── test_helper.exs
│ ├── sample_server_web
│ │ ├── views
│ │ │ ├── page_view_test.exs
│ │ │ ├── layout_view_test.exs
│ │ │ └── error_view_test.exs
│ │ └── controllers
│ │ │ └── page_controller_test.exs
│ └── support
│ │ ├── channel_case.ex
│ │ └── conn_case.ex
├── assets
│ ├── css
│ │ ├── app.css
│ │ └── phoenix.css
│ ├── static
│ │ ├── favicon.ico
│ │ ├── images
│ │ │ └── phoenix.png
│ │ └── robots.txt
│ ├── package.json
│ ├── js
│ │ ├── app.js
│ │ └── socket.js
│ └── brunch-config.js
├── lib
│ ├── sample_server_web
│ │ ├── views
│ │ │ ├── page_view.ex
│ │ │ ├── layout_view.ex
│ │ │ ├── error_view.ex
│ │ │ └── error_helpers.ex
│ │ ├── templates
│ │ │ ├── page
│ │ │ │ └── index.html.eex
│ │ │ └── layout
│ │ │ │ └── app.html.eex
│ │ ├── controllers
│ │ │ └── page_controller.ex
│ │ ├── channels
│ │ │ ├── room_channel.ex
│ │ │ └── user_socket.ex
│ │ ├── router.ex
│ │ ├── gettext.ex
│ │ └── endpoint.ex
│ ├── sample_server.ex
│ ├── sample_server
│ │ └── application.ex
│ └── sample_server_web.ex
├── config
│ ├── test.exs
│ ├── config.exs
│ ├── dev.exs
│ └── prod.exs
├── priv
│ └── gettext
│ │ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ │ └── errors.pot
├── .gitignore
├── README.md
├── mix.exs
└── mix.lock
├── src
├── PhoenixClient
│ ├── ChannelState.cs
│ ├── ChannelEvents.cs
│ ├── RecHook.cs
│ ├── AfterHook.cs
│ ├── Envelope.cs
│ ├── PhoenixClient.csproj
│ ├── Push.cs
│ ├── Channel.cs
│ └── Socket.cs
├── TestClient
│ ├── TestClient.csproj
│ └── Program.cs
└── PhoenixClient.sln
├── README.md
└── .gitignore
/vsn1/README.md:
--------------------------------------------------------------------------------
1 | ## CSharp Phoenix Client
2 |
--------------------------------------------------------------------------------
/sample_server/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 |
--------------------------------------------------------------------------------
/sample_server/assets/css/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.PageView do
2 | use SampleServerWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.LayoutView do
2 | use SampleServerWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/sample_server/assets/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livehelpnow/CSharpPhoenixClient/HEAD/sample_server/assets/static/favicon.ico
--------------------------------------------------------------------------------
/sample_server/assets/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livehelpnow/CSharpPhoenixClient/HEAD/sample_server/assets/static/images/phoenix.png
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_server/test/sample_server_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.PageViewTest do
2 | use SampleServerWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/sample_server/test/sample_server_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.LayoutViewTest do
2 | use SampleServerWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/vsn1/ChannelState.cs:
--------------------------------------------------------------------------------
1 | namespace PhoenixChannels
2 | {
3 | public enum ChannelState
4 | {
5 | Closed,
6 | Errored,
7 | Joined,
8 | Joining,
9 | }
10 | }
--------------------------------------------------------------------------------
/src/PhoenixClient/ChannelState.cs:
--------------------------------------------------------------------------------
1 | namespace PhoenixChannels
2 | {
3 | public enum ChannelState
4 | {
5 | Closed,
6 | Errored,
7 | Joined,
8 | Joining,
9 | }
10 | }
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.PageController do
2 | use SampleServerWeb, :controller
3 |
4 | def index(conn, _params) do
5 | render conn, "index.html"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/vsn1/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample_server/assets/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/sample_server/test/sample_server_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.PageControllerTest do
2 | use SampleServerWeb.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/src/TestClient/TestClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | netcoreapp2.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServer do
2 | @moduledoc """
3 | SampleServer keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/sample_server/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :sample_server, SampleServerWeb.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
--------------------------------------------------------------------------------
/vsn1/ChannelEvents.cs:
--------------------------------------------------------------------------------
1 | namespace PhoenixChannels
2 | {
3 | public static class ChannelEvents
4 | {
5 | public static string Close = "phx_close";
6 | public static string Error = "phx_error";
7 | public static string Join = "phx_join";
8 | public static string Reply = "phx_reply";
9 | public static string Leave = "phx_leave";
10 | }
11 | }
--------------------------------------------------------------------------------
/src/PhoenixClient/ChannelEvents.cs:
--------------------------------------------------------------------------------
1 | namespace PhoenixChannels
2 | {
3 | public static class ChannelEvents
4 | {
5 | public static string Close = "phx_close";
6 | public static string Error = "phx_error";
7 | public static string Join = "phx_join";
8 | public static string Reply = "phx_reply";
9 | public static string Leave = "phx_leave";
10 | }
11 | }
--------------------------------------------------------------------------------
/sample_server/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
--------------------------------------------------------------------------------
/vsn1/RecHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json.Linq;
3 |
4 | namespace PhoenixChannels
5 | {
6 | ///
7 | /// Receive Hook used to register delegates/callbacks which should be executed when message is received from server.
8 | ///
9 | internal class RecHook
10 | {
11 | public string Status { get; set; }
12 | public Action Callback { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/sample_server/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 |
--------------------------------------------------------------------------------
/src/PhoenixClient/RecHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json.Linq;
3 |
4 | namespace PhoenixChannels
5 | {
6 | ///
7 | /// Receive Hook used to register delegates/callbacks which should be executed when message is received from server.
8 | ///
9 | internal class RecHook
10 | {
11 | public string Status { get; set; }
12 | public Action Callback { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/sample_server/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "brunch build --production",
6 | "watch": "brunch watch --stdin"
7 | },
8 | "dependencies": {
9 | "phoenix": "file:../deps/phoenix",
10 | "phoenix_html": "file:../deps/phoenix_html"
11 | },
12 | "devDependencies": {
13 | "babel-brunch": "6.1.1",
14 | "brunch": "2.10.9",
15 | "clean-css-brunch": "2.10.0",
16 | "uglify-js-brunch": "2.10.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/vsn1/AfterHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Timers;
3 |
4 | namespace PhoenixChannels
5 | {
6 | ///
7 | /// Object used to register hooks which should be executed after message is pushed.
8 | ///
9 | /// Only single callback action can be registered in instance.
10 | ///
11 | internal class AfterHook
12 | {
13 | public int Ms { get; set; }
14 | public Action Callback { get; set; }
15 | public Timer Timer { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/PhoenixClient/AfterHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Timers;
3 |
4 | namespace PhoenixChannels
5 | {
6 | ///
7 | /// Object used to register hooks which should be executed after message is pushed.
8 | ///
9 | /// Only single callback action can be registered in instance.
10 | ///
11 | internal class AfterHook
12 | {
13 | public int Ms { get; set; }
14 | public Action Callback { get; set; }
15 | public Timer Timer { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/sample_server/test/sample_server_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.ErrorViewTest do
2 | use SampleServerWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(SampleServerWeb.ErrorView, "404.html", []) ==
9 | "Not Found"
10 | end
11 |
12 | test "renders 500.html" do
13 | assert render_to_string(SampleServerWeb.ErrorView, "500.html", []) ==
14 | "Internal Server Error"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/src/PhoenixClient/Envelope.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using Newtonsoft.Json.Linq;
3 |
4 | namespace PhoenixChannels
5 | {
6 | public class Envelope
7 | {
8 | [JsonProperty("topic")]
9 | public string Topic { get; set; }
10 | [JsonProperty("event")]
11 | public string Event { get; set; }
12 | [JsonProperty("payload")]
13 | public JObject Payload { get; set; }
14 | [JsonProperty("ref")]
15 | public string Ref { get; set; }
16 | [JsonProperty("join_ref")]
17 | public string JoinRef { get; set; }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.ErrorView do
2 | use SampleServerWeb, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/channels/room_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.RoomChannel do
2 | use Phoenix.Channel
3 | require Logger
4 |
5 | def join("room:lobby", %{"user_id" => user_id}, socket) do
6 | {:ok, %{response: "Hi #{user_id}, your are in lobby channel now"}, socket}
7 | end
8 |
9 | def join("room:" <> _private_room_id, _params, _socket) do
10 | {:error, %{reason: "unauthorized"}}
11 | end
12 |
13 | def handle_in("new_msg", %{"body" => body}, socket) do
14 | broadcast!(socket, "new_msg", %{body: body})
15 | {:reply, {:ok, %{message: "sent"}}, socket}
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.Router do
2 | use SampleServerWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | end
15 |
16 | scope "/", SampleServerWeb do
17 | pipe_through :browser # Use the default browser stack
18 |
19 | get "/", PageController, :index
20 | end
21 |
22 | # Other scopes may use custom stacks.
23 | # scope "/api", SampleServerWeb do
24 | # pipe_through :api
25 | # end
26 | end
27 |
--------------------------------------------------------------------------------
/vsn1/Envelope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using WebSocketSharp;
5 | using System.Timers;
6 | using Newtonsoft.Json;
7 | using Newtonsoft.Json.Linq;
8 |
9 | namespace PhoenixChannels
10 | {
11 | public class Envelope
12 | {
13 | [JsonProperty("topic")]
14 | public string Topic { get; set; }
15 | [JsonProperty("event")]
16 | public string Event { get; set; }
17 | [JsonProperty("payload")]
18 | public JObject Payload { get; set; }
19 | [JsonProperty("ref")]
20 | public string Ref { get; set; }
21 | [JsonProperty("join_ref")]
22 | public string JoinRef { get; set; }
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/sample_server/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # Generated on crash by NPM
11 | npm-debug.log
12 |
13 | # Static artifacts
14 | /assets/node_modules
15 |
16 | # Since we are building assets from assets/,
17 | # we ignore priv/static. You may want to comment
18 | # this depending on your deployment strategy.
19 | /priv/static/
20 |
21 | # Files matching config/*.secret.exs pattern contain sensitive
22 | # data and you should not commit them into version control.
23 | #
24 | # Alternatively, you may comment the line below and commit the
25 | # secrets files as long as you replace their contents by environment
26 | # variables.
27 | /config/*.secret.exs
--------------------------------------------------------------------------------
/sample_server/README.md:
--------------------------------------------------------------------------------
1 | # SampleServer
2 |
3 | To start your Phoenix server:
4 |
5 | * Install dependencies with `mix deps.get`
6 | * Install Node.js dependencies with `cd assets && npm install`
7 | * Start Phoenix endpoint with `mix phx.server`
8 |
9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
10 |
11 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).
12 |
13 | ## Learn more
14 |
15 | * Official website: http://www.phoenixframework.org/
16 | * Guides: http://phoenixframework.org/docs/overview
17 | * Docs: https://hexdocs.pm/phoenix
18 | * Mailing list: http://groups.google.com/group/phoenix-talk
19 | * Source: https://github.com/phoenixframework/phoenix
20 |
--------------------------------------------------------------------------------
/sample_server/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // Brunch automatically concatenates all files in your
2 | // watched paths. Those paths can be configured at
3 | // config.paths.watched in "brunch-config.js".
4 | //
5 | // However, those files will only be executed if
6 | // explicitly imported. The only exception are files
7 | // in vendor, which are never wrapped in imports and
8 | // therefore are always executed.
9 |
10 | // Import dependencies
11 | //
12 | // If you no longer want to use a dependency, remember
13 | // to also remove its path from "config.paths.watched".
14 | import "phoenix_html";
15 |
16 | // Import local files
17 | //
18 | // Local files can be imported directly using relative
19 | // paths "./socket" or full ones "web/static/js/socket".
20 |
21 | import socket from "./socket";
22 |
--------------------------------------------------------------------------------
/vsn1/CSharpPhoenixClient.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhoenixClient", "PhoenixClient.csproj", "{C882E926-39B3-4256-881E-E0519C9634D5}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {C882E926-39B3-4256-881E-E0519C9634D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {C882E926-39B3-4256-881E-E0519C9634D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {C882E926-39B3-4256-881E-E0519C9634D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {C882E926-39B3-4256-881E-E0519C9634D5}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import SampleServerWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :sample_server
24 | end
25 |
--------------------------------------------------------------------------------
/vsn1/PhoenixClient.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2012
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhoenixClient", "PhoenixClient.csproj", "{C882E926-39B3-4256-881E-E0519C9634D5}"
5 | EndProject
6 | Global
7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
8 | Debug|Any CPU = Debug|Any CPU
9 | Release|Any CPU = Release|Any CPU
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {C882E926-39B3-4256-881E-E0519C9634D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13 | {C882E926-39B3-4256-881E-E0519C9634D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
14 | {C882E926-39B3-4256-881E-E0519C9634D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
15 | {C882E926-39B3-4256-881E-E0519C9634D5}.Release|Any CPU.Build.0 = Release|Any CPU
16 | EndGlobalSection
17 | EndGlobal
18 |
--------------------------------------------------------------------------------
/sample_server/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | # The default endpoint for testing
24 | @endpoint SampleServerWeb.Endpoint
25 | end
26 | end
27 |
28 |
29 | setup _tags do
30 | :ok
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/sample_server/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # Configures the endpoint
9 | config :sample_server, SampleServerWeb.Endpoint,
10 | url: [host: "localhost"],
11 | secret_key_base: "s0MOxv6ZYkY8Yi+07yuJoa/QreSH4VZXDtdx9rMq2MxDIiw+r3RCv3M3gfzy2y9o",
12 | render_errors: [view: SampleServerWeb.ErrorView, accepts: ~w(html json)],
13 | pubsub: [name: SampleServer.PubSub,
14 | adapter: Phoenix.PubSub.PG2]
15 |
16 | # Configures Elixir's Logger
17 | config :logger, :console,
18 | format: "$time $metadata[$level] $message\n",
19 | metadata: [:user_id]
20 |
21 | # Import environment specific config. This must remain at the bottom
22 | # of this file so it overrides the configuration defined above.
23 | import_config "#{Mix.env}.exs"
24 |
--------------------------------------------------------------------------------
/sample_server/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 | import SampleServerWeb.Router.Helpers
23 |
24 | # The default endpoint for testing
25 | @endpoint SampleServerWeb.Endpoint
26 | end
27 | end
28 |
29 |
30 | setup _tags do
31 | {:ok, conn: Phoenix.ConnTest.build_conn()}
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/src/PhoenixClient/PhoenixClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | PhoenixChannels
5 | PhoenixChannels
6 | 0.1.0
7 | Milan Jaric
8 | LiveHelpNow LLC.
9 | Phoenix Channels
10 | Phoenixframework Scoket Client library for .NET Framework, .NET Core, .NET Standard
11 | https://github.com/livehelpnow/CSharpPhoenixClient
12 | https://github.com/livehelpnow/CSharpPhoenixClient
13 | Git
14 | Websocket, Phoenixframework, Phoenix, Channels
15 | true
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server/application.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServer.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the endpoint when the application starts
12 | supervisor(SampleServerWeb.Endpoint, []),
13 | # Start your own worker by calling: SampleServer.Worker.start_link(arg1, arg2, arg3)
14 | # worker(SampleServer.Worker, [arg1, arg2, arg3]),
15 | ]
16 |
17 | # See https://hexdocs.pm/elixir/Supervisor.html
18 | # for other strategies and supported options
19 | opts = [strategy: :one_for_one, name: SampleServer.Supervisor]
20 | Supervisor.start_link(children, opts)
21 | end
22 |
23 | # Tell Phoenix to update the endpoint configuration
24 | # whenever the application is updated.
25 | def config_change(changed, _new, removed) do
26 | SampleServerWeb.Endpoint.config_change(changed, removed)
27 | :ok
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/sample_server/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule SampleServer.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :sample_server,
7 | version: "0.0.1",
8 | elixir: "~> 1.4",
9 | elixirc_paths: elixirc_paths(Mix.env),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
11 | start_permanent: Mix.env == :prod,
12 | deps: deps()
13 | ]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [
21 | mod: {SampleServer.Application, []},
22 | extra_applications: [:logger, :runtime_tools]
23 | ]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "test/support"]
28 | defp elixirc_paths(_), do: ["lib"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.3.3"},
36 | {:phoenix_pubsub, "~> 1.0"},
37 | {:phoenix_html, "~> 2.10"},
38 | {:phoenix_live_reload, "~> 1.0", only: :dev},
39 | {:gettext, "~> 0.11"},
40 | {:cowboy, "~> 1.0"}
41 | ]
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello SampleServer!
11 | ">
12 |
13 |
14 |
15 |
16 |
24 |
25 |
<%= get_flash(@conn, :info) %>
26 |
<%= get_flash(@conn, :error) %>
27 |
28 |
29 | <%= render @view_module, @view_template, assigns %>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | channel "room:*", SampleServerWeb.RoomChannel
6 |
7 | ## Transports
8 | transport :websocket, Phoenix.Transports.WebSocket
9 | # transport :longpoll, Phoenix.Transports.LongPoll
10 |
11 | # Socket params are passed from the client and can
12 | # be used to verify and authenticate a user. After
13 | # verification, you can put default assigns into
14 | # the socket that will be set for all channels, ie
15 | #
16 | # {:ok, assign(socket, :user_id, verified_user_id)}
17 | #
18 | # To deny connection, return `:error`.
19 | #
20 | # See `Phoenix.Token` documentation for examples in
21 | # performing token verification on connect.
22 | def connect(_params, socket) do
23 | {:ok, socket}
24 | end
25 |
26 | # Socket id's are topics that allow you to identify all sockets for a given user:
27 | #
28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
29 | #
30 | # Would allow you to broadcast a "disconnect" event and terminate
31 | # all active sockets and channels for a given user:
32 | #
33 | # SampleServerWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
34 | #
35 | # Returning `nil` makes this socket anonymous.
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/vsn1/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("PhoenixChannels")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("PhoenixChannels")]
13 | [assembly: AssemblyCopyright("Copyright © 2015")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("a380c313-bdeb-48eb-a87b-7435b0eb826e")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("2.0.0.0")]
36 | [assembly: AssemblyFileVersion("2.0.0.0")]
--------------------------------------------------------------------------------
/src/PhoenixClient.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhoenixClient", "PhoenixClient\PhoenixClient.csproj", "{59AD55FE-9C81-41D7-80F5-3D18FB09F443}"
4 | EndProject
5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Markdown", "Markdown", "{7DD6E04C-48BB-4521-AF76-BBAD31DFCFD6}"
6 | ProjectSection(SolutionItems) = preProject
7 | ..\README.md = ..\README.md
8 | EndProjectSection
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestClient", "TestClient\TestClient.csproj", "{61F57066-02B4-4033-A327-9295265AD8E2}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {59AD55FE-9C81-41D7-80F5-3D18FB09F443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {59AD55FE-9C81-41D7-80F5-3D18FB09F443}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {59AD55FE-9C81-41D7-80F5-3D18FB09F443}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {59AD55FE-9C81-41D7-80F5-3D18FB09F443}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {61F57066-02B4-4033-A327-9295265AD8E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {61F57066-02B4-4033-A327-9295265AD8E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {61F57066-02B4-4033-A327-9295265AD8E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {61F57066-02B4-4033-A327-9295265AD8E2}.Release|Any CPU.Build.0 = Release|Any CPU
26 | EndGlobalSection
27 | EndGlobal
28 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # When using gettext, we typically pass the strings we want
22 | # to translate as a static argument:
23 | #
24 | # # Translate "is invalid" in the "errors" domain
25 | # dgettext "errors", "is invalid"
26 | #
27 | # # Translate the number of files with plural rules
28 | # dngettext "errors", "1 file", "%{count} files", count
29 | #
30 | # Because the error messages we show in our forms and APIs
31 | # are defined inside Ecto, we need to translate them dynamically.
32 | # This requires us to call the Gettext module passing our gettext
33 | # backend as first argument.
34 | #
35 | # Note we use the "errors" domain, which means translations
36 | # should be written to the errors.po file. The :count option is
37 | # set by Ecto and indicates we should also apply plural rules.
38 | if count = opts[:count] do
39 | Gettext.dngettext(SampleServerWeb.Gettext, "errors", msg, msg, count, opts)
40 | else
41 | Gettext.dgettext(SampleServerWeb.Gettext, "errors", msg, opts)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/sample_server/assets/brunch-config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // See http://brunch.io/#documentation for docs.
3 | files: {
4 | javascripts: {
5 | joinTo: "js/app.js"
6 |
7 | // To use a separate vendor.js bundle, specify two files path
8 | // http://brunch.io/docs/config#-files-
9 | // joinTo: {
10 | // "js/app.js": /^js/,
11 | // "js/vendor.js": /^(?!js)/
12 | // }
13 | //
14 | // To change the order of concatenation of files, explicitly mention here
15 | // order: {
16 | // before: [
17 | // "vendor/js/jquery-2.1.1.js",
18 | // "vendor/js/bootstrap.min.js"
19 | // ]
20 | // }
21 | },
22 | stylesheets: {
23 | joinTo: "css/app.css"
24 | },
25 | templates: {
26 | joinTo: "js/app.js"
27 | }
28 | },
29 |
30 | conventions: {
31 | // This option sets where we should place non-css and non-js assets in.
32 | // By default, we set this to "/assets/static". Files in this directory
33 | // will be copied to `paths.public`, which is "priv/static" by default.
34 | assets: /^(static)/
35 | },
36 |
37 | // Phoenix paths configuration
38 | paths: {
39 | // Dependencies and current project directories to watch
40 | watched: ["static", "css", "js", "vendor"],
41 | // Where to compile files to
42 | public: "../priv/static"
43 | },
44 |
45 | // Configure your plugins
46 | plugins: {
47 | babel: {
48 | // Do not use ES6 compiler in vendor code
49 | ignore: [/vendor/]
50 | }
51 | },
52 |
53 | modules: {
54 | autoRequire: {
55 | "js/app.js": ["js/app"]
56 | }
57 | },
58 |
59 | npm: {
60 | enabled: true
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :sample_server
3 |
4 | socket "/socket", SampleServerWeb.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :sample_server, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.Logger
23 |
24 | plug Plug.Parsers,
25 | parsers: [:urlencoded, :multipart, :json],
26 | pass: ["*/*"],
27 | json_decoder: Poison
28 |
29 | plug Plug.MethodOverride
30 | plug Plug.Head
31 |
32 | # The session will be stored in the cookie and signed,
33 | # this means its contents can be read but not tampered with.
34 | # Set :encryption_salt if you would also like to encrypt it.
35 | plug Plug.Session,
36 | store: :cookie,
37 | key: "_sample_server_key",
38 | signing_salt: "iFNDk0Ry"
39 |
40 | plug SampleServerWeb.Router
41 |
42 | @doc """
43 | Callback invoked for dynamically configuring the endpoint.
44 |
45 | It receives the endpoint configuration and checks if
46 | configuration should be loaded from the system environment.
47 | """
48 | def init(_key, config) do
49 | if config[:load_from_system_env] do
50 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
51 | {:ok, Keyword.put(config, :http, [:inet6, port: port])}
52 | else
53 | {:ok, config}
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/sample_server/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :sample_server, SampleServerWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
15 | cd: Path.expand("../assets", __DIR__)]]
16 |
17 | # ## SSL Support
18 | #
19 | # In order to use HTTPS in development, a self-signed
20 | # certificate can be generated by running the following
21 | # command from your terminal:
22 | #
23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
24 | #
25 | # The `http:` config above can be replaced with:
26 | #
27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
28 | #
29 | # If desired, both `http:` and `https:` keys can be
30 | # configured to run both http and https servers on
31 | # different ports.
32 |
33 | # Watch static and templates for browser reloading.
34 | config :sample_server, SampleServerWeb.Endpoint,
35 | live_reload: [
36 | patterns: [
37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
38 | ~r{priv/gettext/.*(po)$},
39 | ~r{lib/sample_server_web/views/.*(ex)$},
40 | ~r{lib/sample_server_web/templates/.*(eex)$}
41 | ]
42 | ]
43 |
44 | # Do not include metadata nor timestamps in development logs
45 | config :logger, :console, format: "[$level] $message\n"
46 |
47 | # Set a higher stacktrace during development. Avoid configuring such
48 | # in production as building large stacktraces may be expensive.
49 | config :phoenix, :stacktrace_depth, 20
50 |
--------------------------------------------------------------------------------
/sample_server/lib/sample_server_web.ex:
--------------------------------------------------------------------------------
1 | defmodule SampleServerWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use SampleServerWeb, :controller
9 | use SampleServerWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: SampleServerWeb
23 | import Plug.Conn
24 | import SampleServerWeb.Router.Helpers
25 | import SampleServerWeb.Gettext
26 | end
27 | end
28 |
29 | def view do
30 | quote do
31 | use Phoenix.View, root: "lib/sample_server_web/templates",
32 | namespace: SampleServerWeb
33 |
34 | # Import convenience functions from controllers
35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
36 |
37 | # Use all HTML functionality (forms, tags, etc)
38 | use Phoenix.HTML
39 |
40 | import SampleServerWeb.Router.Helpers
41 | import SampleServerWeb.ErrorHelpers
42 | import SampleServerWeb.Gettext
43 | end
44 | end
45 |
46 | def router do
47 | quote do
48 | use Phoenix.Router
49 | import Plug.Conn
50 | import Phoenix.Controller
51 | end
52 | end
53 |
54 | def channel do
55 | quote do
56 | use Phoenix.Channel
57 | import SampleServerWeb.Gettext
58 | end
59 | end
60 |
61 | @doc """
62 | When used, dispatch to the appropriate controller/view/etc.
63 | """
64 | defmacro __using__(which) when is_atom(which) do
65 | apply(__MODULE__, which, [])
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/sample_server/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
3 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
4 | "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
5 | "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
6 | "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
7 | "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
8 | "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
9 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.5", "8d4c9b1ef9ca82deee6deb5a038d6d8d7b34b9bb909d99784a49332e0d15b3dc", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
10 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.0", "d55e25ff1ff8ea2f9964638366dfd6e361c52dedfd50019353598d11d4441d14", [:mix], [], "hexpm"},
11 | "plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
12 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
13 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
14 | }
15 |
--------------------------------------------------------------------------------
/sample_server/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we often load configuration from external
4 | # sources, such as your system environment. For this reason,
5 | # you won't find the :http configuration below, but set inside
6 | # SampleServerWeb.Endpoint.init/2 when load_from_system_env is
7 | # true. Any dynamic configuration should be done there.
8 | #
9 | # Don't forget to configure the url host to something meaningful,
10 | # Phoenix uses this information when generating URLs.
11 | #
12 | # Finally, we also include the path to a cache manifest
13 | # containing the digested version of static files. This
14 | # manifest is generated by the mix phx.digest task
15 | # which you typically run after static files are built.
16 | config :sample_server, SampleServerWeb.Endpoint,
17 | load_from_system_env: true,
18 | url: [host: "example.com", port: 80],
19 | cache_static_manifest: "priv/static/cache_manifest.json"
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # ## SSL Support
25 | #
26 | # To get SSL working, you will need to add the `https` key
27 | # to the previous section and set your `:url` port to 443:
28 | #
29 | # config :sample_server, SampleServerWeb.Endpoint,
30 | # ...
31 | # url: [host: "example.com", port: 443],
32 | # https: [:inet6,
33 | # port: 443,
34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
36 | #
37 | # Where those two env variables return an absolute path to
38 | # the key and cert in disk or a relative path inside priv,
39 | # for example "priv/ssl/server.key".
40 | #
41 | # We also recommend setting `force_ssl`, ensuring no data is
42 | # ever sent via http, always redirecting to https:
43 | #
44 | # config :sample_server, SampleServerWeb.Endpoint,
45 | # force_ssl: [hsts: true]
46 | #
47 | # Check `Plug.SSL` for all available options in `force_ssl`.
48 |
49 | # ## Using releases
50 | #
51 | # If you are doing OTP releases, you need to instruct Phoenix
52 | # to start the server for all endpoints:
53 | #
54 | # config :phoenix, :serve_endpoints, true
55 | #
56 | # Alternatively, you can configure exactly which server to
57 | # start per endpoint:
58 | #
59 | # config :sample_server, SampleServerWeb.Endpoint, server: true
60 | #
61 |
62 | # Finally import the config/prod.secret.exs
63 | # which should be versioned separately.
64 | import_config "prod.secret.exs"
65 |
--------------------------------------------------------------------------------
/sample_server/assets/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "assets/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket
5 | // and connect at the socket path in "lib/web/endpoint.ex":
6 | import {
7 | Socket
8 | } from "phoenix";
9 |
10 | let socket = new Socket("/socket", {
11 | params: {
12 | token: window.userToken
13 | }
14 | });
15 |
16 | // When you connect, you'll often need to authenticate the client.
17 | // For example, imagine you have an authentication plug, `MyAuth`,
18 | // which authenticates the session and assigns a `:current_user`.
19 | // If the current user exists you can assign the user's token in
20 | // the connection for use in the layout.
21 | //
22 | // In your "lib/web/router.ex":
23 | //
24 | // pipeline :browser do
25 | // ...
26 | // plug MyAuth
27 | // plug :put_user_token
28 | // end
29 | //
30 | // defp put_user_token(conn, _) do
31 | // if current_user = conn.assigns[:current_user] do
32 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
33 | // assign(conn, :user_token, token)
34 | // else
35 | // conn
36 | // end
37 | // end
38 | //
39 | // Now you need to pass this token to JavaScript. You can do so
40 | // inside a script tag in "lib/web/templates/layout/app.html.eex":
41 | //
42 | //
43 | //
44 | // You will need to verify the user token in the "connect/2" function
45 | // in "lib/web/channels/user_socket.ex":
46 | //
47 | // def connect(%{"token" => token}, socket) do
48 | // # max_age: 1209600 is equivalent to two weeks in seconds
49 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
50 | // {:ok, user_id} ->
51 | // {:ok, assign(socket, :user, user_id)}
52 | // {:error, reason} ->
53 | // :error
54 | // end
55 | // end
56 | //
57 | // Finally, pass the token on connect as below. Or remove it
58 | // from connect if you don't care about authentication.
59 |
60 | socket.connect();
61 |
62 | // Now that you are connected, you can join channels with a topic:
63 | let channel = socket.channel("room:lobby", {
64 | user_id: Math.round(Math.random() * 1000)
65 | });
66 | let chatInput = document.querySelector("#chat-input");
67 | let messagesContainer = document.querySelector("#messages");
68 |
69 | chatInput
70 | .addEventListener("keypress", event => {
71 | if (event.keyCode === 13) {
72 | channel
73 | .push("new_msg", {
74 | body: chatInput.value
75 | })
76 | .receive("ok", console.info.bind(console))
77 | .receive("error", console.error.bind(console));
78 | chatInput.value = "";
79 | }
80 | });
81 |
82 | channel
83 | .on("new_msg", payload => {
84 | let messageItem = document.createElement("li");
85 | messageItem.innerText = `[${Date()}] ${payload.body}`;
86 | messagesContainer.appendChild(messageItem);
87 | });
88 |
89 |
90 | channel
91 | .join()
92 | .receive("ok", resp => {
93 | console.log("Joined successfully", resp);
94 | })
95 | .receive("error", resp => {
96 | console.log("Unable to join", resp);
97 | });
98 |
99 |
100 | export default socket;
--------------------------------------------------------------------------------
/src/TestClient/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json.Linq;
3 | using PhoenixChannels;
4 |
5 | namespace TestClient
6 | {
7 | class Program
8 | {
9 |
10 | private Socket _socket;
11 | private Channel _chan;
12 | public Program()
13 | {
14 | _socket = new Socket("ws://localhost:4000/socket");
15 | _socket.OnOpen(OnConnect);
16 | _socket.OnClose(OnDisconnect);
17 | _socket.OnError(OnError);
18 | }
19 |
20 | public void Start()
21 | {
22 | _socket.Connect();
23 | Info("Application Started");
24 | }
25 |
26 | private void OnConnect()
27 | {
28 | Info("Connected...");
29 | var @params = new JObject
30 | {
31 | ["user_id"] = "TestClient"
32 | };
33 | var topic = "room:lobby";
34 | _chan = _socket.Chan(topic, @params);
35 | _chan.On("new_msg", OnLobbyMessage);
36 | _chan.Join()
37 | .Receive("ok", OnLobbyJoinSuccess)
38 | .Receive("error", OnLobbyJoinError);
39 | }
40 |
41 | private void OnLobbyMessage(JObject arg1, string _)
42 | {
43 | Info($"Message from Lobby: {arg1["body"]}");
44 | }
45 |
46 | private void OnLobbyJoinError(JObject obj)
47 | {
48 | Error($"Failed to join to lobby channel due {obj}");
49 | }
50 |
51 | private void OnLobbyJoinSuccess(JObject obj)
52 | {
53 | Info($"Successfuly joined to lobby got message: {obj}");
54 | var hiMessage = new JObject
55 | {
56 | ["body"] = "Hello phoenix!"
57 | };
58 | _chan.Push("new_msg", hiMessage)
59 | .Receive("ok", o =>
60 | {
61 | Info("Succesfully pushed message to lobby.");
62 | }).Receive("error", e =>
63 | {
64 | Error($"Failed to push message to pheoenix channel lobby, due {e}");
65 | });
66 | }
67 |
68 | private void OnError(ErrorEventArgs args)
69 | {
70 | Error($"Socket error {args.Reason}");
71 | }
72 |
73 | private void OnDisconnect(CloseEventArgs args)
74 | {
75 | Warn($"Socket disconnected {args.Reason}");
76 | }
77 |
78 | private void Info(string message)
79 | {
80 | Log($"[INFO] {message}", ConsoleColor.Green);
81 | }
82 |
83 | private void Error(string message)
84 | {
85 | Log($"[ERROR] {message}", ConsoleColor.Red);
86 | }
87 |
88 | private void Warn(string message)
89 | {
90 | Log($"[WARNING] {message}", ConsoleColor.Yellow);
91 | }
92 |
93 | private void Log(string message, ConsoleColor color)
94 | {
95 | Console.ForegroundColor = color;
96 | Console.WriteLine($"[{DateTime.Now.ToString("yyyy-MM-dd'T'HH:mm:ss zzz")}] {message}");
97 | Console.ResetColor();
98 | }
99 |
100 | static void Main(string[] _)
101 | {
102 |
103 | var chatApp = new Program();
104 |
105 | chatApp.Start();
106 | var key = Console.ReadKey();
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/vsn1/PhoenixClient.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {C882E926-39B3-4256-881E-E0519C9634D5}
8 | Library
9 | Properties
10 | PhoenixChannels
11 | PhoenixChannels
12 | v4.5
13 | 512
14 |
15 |
16 | true
17 | full
18 | false
19 | bin\Debug\
20 | DEBUG;TRACE
21 | prompt
22 | 4
23 |
24 |
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 | packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll
35 | True
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | packages\WebSocketSharp.1.0.3-rc9\lib\websocket-sharp.dll
46 | True
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # C# Phoenixframework Client Library
2 |
3 | Supports any platform that can run .NET Standard 2 SDK. API is built to look close to official phoenix.js API. Check example below to see usage.
4 |
5 | ## Simaple Usage
6 |
7 | ```c#
8 | using System;
9 | using Newtonsoft.Json.Linq;
10 | using PhoenixChannels;
11 |
12 | namespace TestClient
13 | {
14 | class Program
15 | {
16 |
17 | private Socket _socket;
18 | private Channel _chan;
19 | public Program()
20 | {
21 | _socket = new Socket("ws://localhost:4000/socket");
22 | _socket.OnOpen(OnConnect);
23 | _socket.OnClose(OnDisconnect);
24 | _socket.OnError(OnError);
25 | }
26 |
27 | public void Start()
28 | {
29 | _socket.Connect();
30 | Info("Application Started");
31 | }
32 |
33 | private void OnConnect()
34 | {
35 | Info("Connected...");
36 | var @params = new JObject
37 | {
38 | ["user_id"] = "TestClient"
39 | };
40 | var topic = "room:lobby";
41 | _chan = _socket.Chan(topic, @params);
42 | _chan.On("new_msg", OnLobbyMessage);
43 | _chan.Join()
44 | .Receive("ok", OnLobbyJoinSuccess)
45 | .Receive("error", OnLobbyJoinError);
46 | }
47 |
48 | private void OnLobbyMessage(JObject arg1, string _)
49 | {
50 | Info($"Message from Lobby: {arg1["body"]}");
51 | }
52 |
53 | private void OnLobbyJoinError(JObject obj)
54 | {
55 | Error($"Failed to join to lobby channel due {obj}");
56 | }
57 |
58 | private void OnLobbyJoinSuccess(JObject obj)
59 | {
60 | Info($"Successfuly joined to lobby got message: {obj}");
61 | var hiMessage = new JObject
62 | {
63 | ["body"] = "Hello phoenix!"
64 | };
65 | _chan.Push("new_msg", hiMessage)
66 | .Receive("ok", o =>
67 | {
68 | Info("Succesfully pushed message to lobby.");
69 | }).Receive("error", e =>
70 | {
71 | Error($"Failed to push message to pheoenix channel lobby, due {e}");
72 | });
73 | }
74 |
75 | private void OnError(ErrorEventArgs args)
76 | {
77 | Error($"Socket error {args.Reason}");
78 | }
79 |
80 | private void OnDisconnect(CloseEventArgs args)
81 | {
82 | Warn($"Socket disconnected {args.Reason}");
83 | }
84 |
85 | private void Info(string message)
86 | {
87 | Log($"[INFO] {message}", ConsoleColor.Green);
88 | }
89 |
90 | private void Error(string message)
91 | {
92 | Log($"[ERROR] {message}", ConsoleColor.Red);
93 | }
94 |
95 | private void Warn(string message)
96 | {
97 | Log($"[WARNING] {message}", ConsoleColor.Yellow);
98 | }
99 |
100 | private void Log(string message, ConsoleColor color)
101 | {
102 | Console.ForegroundColor = color;
103 | Console.WriteLine($"[{DateTime.Now.ToString("yyyy-MM-dd'T'HH:mm:ss zzz")}] {message}");
104 | Console.ResetColor();
105 | }
106 |
107 | static void Main(string[] _)
108 | {
109 |
110 | var chatApp = new Program();
111 |
112 | chatApp.Start();
113 | var key = Console.ReadKey();
114 | }
115 | }
116 | }
117 | ```
--------------------------------------------------------------------------------
/vsn1/Push.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Timers;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace PhoenixChannels
7 | {
8 | public class Push
9 | {
10 | private Channel _channel;
11 | private string _event;
12 | private JObject _payload;
13 |
14 | private JObject _receivedResp;
15 | private AfterHook _afterHook;
16 | private List _recHooks;
17 | private bool _sent;
18 |
19 | private string _ref;
20 | private string _refEvent;
21 |
22 | public Push(Channel channel, string event_, JObject payload)
23 | {
24 | _channel = channel;
25 | _event = event_;
26 | _payload = payload;
27 |
28 | _receivedResp = null;
29 | _afterHook = null;
30 | _recHooks = new List();
31 | _sent = false;
32 | }
33 |
34 | public void Send()
35 | {
36 | _ref = _channel.Socket.MakeRef();
37 | _refEvent = _channel.ReplyEventName(_ref);
38 | _receivedResp = null;
39 | _sent = false;
40 |
41 | _channel.On(_refEvent, (payload, reference) =>
42 | {
43 | _receivedResp = payload;
44 | MatchReceive(payload);
45 | CancelRefEvent();
46 | CancelAfter();
47 | });
48 |
49 | StartAfter();
50 | _sent = true;
51 |
52 |
53 | var env = new Envelope
54 | {
55 | Topic = _channel.Topic,
56 | Event = _event,
57 | Payload = _payload,
58 | Ref = _ref,
59 | JoinRef = _channel.JoinRef
60 | };
61 |
62 | _channel.Socket.Push(env);
63 | }
64 |
65 | public Push Receive(string status, Action callback)
66 | {
67 | if (_receivedResp != null && (string) _receivedResp["status"] == status)
68 | {
69 | callback((JObject) _receivedResp["response"]);
70 | }
71 |
72 | _recHooks.Add(new RecHook() {Status = status, Callback = callback});
73 |
74 | return this;
75 | }
76 |
77 | public Push After(int timeoutMs, Action callback)
78 | {
79 | if (_afterHook != null)
80 | {
81 | //throw
82 | throw new Exception("only a single after hook can be applied to a push");
83 | }
84 |
85 | var timer = new Timer(timeoutMs);
86 | if (_sent)
87 | {
88 | timer.Elapsed += (o, e) => callback();
89 | timer.AutoReset = false;
90 | timer.Start(); //.Enabled = true;
91 | }
92 |
93 | _afterHook = new AfterHook {Ms = timeoutMs, Callback = callback, Timer = timer};
94 |
95 | return this;
96 | }
97 |
98 | //// private
99 | private void MatchReceive(JObject payload) //string status, object response, int reference)
100 | {
101 | foreach (var rh in _recHooks)
102 | {
103 | if (rh.Status == (string) payload["status"])
104 | {
105 | rh.Callback((JObject) payload["response"]);
106 | }
107 | }
108 | }
109 |
110 | private void CancelRefEvent()
111 | {
112 | _channel.Off(_refEvent);
113 | }
114 |
115 | private void CancelAfter()
116 | {
117 | if (_afterHook == null) return;
118 | //reset timer
119 | _afterHook.Timer.Stop();
120 | //_afterHook.Timer.Enabled = false;
121 | _afterHook.Timer = null;
122 | }
123 |
124 | private void StartAfter()
125 | {
126 | if (_afterHook == null) return;
127 |
128 | Action callback = () =>
129 | {
130 | CancelRefEvent();
131 | _afterHook.Callback();
132 | };
133 | _afterHook.Timer = new Timer(_afterHook.Ms);
134 | _afterHook.Timer.Elapsed += (o, e) => callback();
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/src/PhoenixClient/Push.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Timers;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace PhoenixChannels
7 | {
8 | public class Push
9 | {
10 | private Channel _channel;
11 | private string _event;
12 | private JObject _payload;
13 |
14 | private JObject _receivedResp;
15 | private AfterHook _afterHook;
16 | private List _recHooks;
17 | private bool _sent;
18 |
19 | private string _ref;
20 | private string _refEvent;
21 |
22 | public Push(Channel channel, string event_, JObject payload)
23 | {
24 | _channel = channel;
25 | _event = event_;
26 | _payload = payload;
27 |
28 | _receivedResp = null;
29 | _afterHook = null;
30 | _recHooks = new List();
31 | _sent = false;
32 | }
33 |
34 | public void Send()
35 | {
36 | _ref = _channel.Socket.MakeRef();
37 | _refEvent = _channel.ReplyEventName(_ref);
38 | _receivedResp = null;
39 | _sent = false;
40 |
41 | _channel.On(_refEvent, (payload, reference) =>
42 | {
43 | _receivedResp = payload;
44 | MatchReceive(payload);
45 | CancelRefEvent();
46 | CancelAfter();
47 | });
48 |
49 | StartAfter();
50 | _sent = true;
51 |
52 |
53 | var env = new Envelope
54 | {
55 | Topic = _channel.Topic,
56 | Event = _event,
57 | Payload = _payload,
58 | Ref = _ref,
59 | JoinRef = _channel.JoinRef
60 | };
61 |
62 | _channel.Socket.Push(env);
63 | }
64 |
65 | public Push Receive(string status, Action callback)
66 | {
67 | if (_receivedResp != null && (string) _receivedResp["status"] == status)
68 | {
69 | callback((JObject) _receivedResp["response"]);
70 | }
71 |
72 | _recHooks.Add(new RecHook() {Status = status, Callback = callback});
73 |
74 | return this;
75 | }
76 |
77 | public Push After(int timeoutMs, Action callback)
78 | {
79 | if (_afterHook != null)
80 | {
81 | //throw
82 | throw new Exception("only a single after hook can be applied to a push");
83 | }
84 |
85 | var timer = new Timer(timeoutMs);
86 | if (_sent)
87 | {
88 | timer.Elapsed += (o, e) => callback();
89 | timer.AutoReset = false;
90 | timer.Start(); //.Enabled = true;
91 | }
92 |
93 | _afterHook = new AfterHook {Ms = timeoutMs, Callback = callback, Timer = timer};
94 |
95 | return this;
96 | }
97 |
98 | //// private
99 | private void MatchReceive(JObject payload) //string status, object response, int reference)
100 | {
101 | foreach (var rh in _recHooks)
102 | {
103 | if (rh.Status == (string) payload["status"])
104 | {
105 | rh.Callback((JObject) payload["response"]);
106 | }
107 | }
108 | }
109 |
110 | private void CancelRefEvent()
111 | {
112 | _channel.Off(_refEvent);
113 | }
114 |
115 | private void CancelAfter()
116 | {
117 | if (_afterHook == null) return;
118 | //reset timer
119 | _afterHook.Timer.Stop();
120 | //_afterHook.Timer.Enabled = false;
121 | _afterHook.Timer = null;
122 | }
123 |
124 | private void StartAfter()
125 | {
126 | if (_afterHook == null) return;
127 |
128 | Action callback = () =>
129 | {
130 | CancelRefEvent();
131 | _afterHook.Callback();
132 | };
133 | _afterHook.Timer = new Timer(_afterHook.Ms);
134 | _afterHook.Timer.Elapsed += (o, e) => callback();
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/src/PhoenixClient/Channel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Timers;
4 | using Newtonsoft.Json.Linq;
5 |
6 | namespace PhoenixChannels
7 | {
8 | public class Channel
9 | {
10 | private ChannelState _state;
11 |
12 | public string Topic { get; set; }
13 |
14 | public Socket Socket { get; set; }
15 |
16 | public string JoinRef { get; private set; }
17 |
18 |
19 | private IDictionary>> _bindings;
20 | private bool _alreadyJoinedOnce;
21 | private Push _joinPush;
22 | private IList _pushBuffer;
23 | private Timer _rejoinTimer;
24 |
25 | public Channel(string topic, JObject params_, Socket socket)
26 | {
27 | _state = ChannelState.Closed;
28 | Topic = topic;
29 |
30 | Socket = socket;
31 | _bindings = new Dictionary>>();
32 | _alreadyJoinedOnce = false;
33 | _pushBuffer = new List();
34 |
35 | JoinRef = Socket.MakeRef();
36 |
37 | _joinPush = new Push(this, ChannelEvents.Join, params_);
38 | _joinPush.Receive("ok", x =>
39 | {
40 | _state = ChannelState.Joined;
41 | });
42 |
43 | OnClose((o, reference) =>
44 | {
45 | _state = ChannelState.Closed;
46 | Socket.Remove(this);
47 | });
48 |
49 | OnError((reason, reference) => //reason is not used
50 | {
51 | _state = ChannelState.Errored;
52 | _rejoinTimer.Start();
53 |
54 | });
55 |
56 | On(ChannelEvents.Reply, (payload, reference) =>
57 | {
58 | Trigger(ReplyEventName(reference), payload, reference);
59 | });
60 |
61 |
62 | _rejoinTimer = new Timer(Socket.ReconnectAfterMs);
63 | _rejoinTimer.AutoReset = false;
64 | _rejoinTimer.Elapsed += (o, e) => RejoinUntilConnected();
65 | //_rejoinTimer.Enabled = true;
66 | }
67 | private void RejoinUntilConnected()
68 | {
69 | if (_state != ChannelState.Errored) return;
70 |
71 | if (Socket.IsConnected())
72 | {
73 | Rejoin();
74 | }
75 | else
76 | {
77 | _rejoinTimer.Start();
78 | }
79 | }
80 |
81 | public Push Join()
82 | {
83 | if (_alreadyJoinedOnce)
84 | {
85 | throw new Exception("tried to join mulitple times. 'join' can only be called a singe time per channel instance");
86 | }
87 | else
88 | {
89 | _alreadyJoinedOnce = true;
90 | }
91 |
92 | SendJoin();
93 |
94 | return _joinPush;
95 | }
96 |
97 | public void OnClose(Action