├── LICENSE
├── README.md
├── mod_gcm.spec
└── src
└── mod_gcm.erl
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Roman K.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | mod_gcm
2 | =====
3 |
4 | **[Fork Me](https://github.com/mrDoctorWho/ejabberd_mod_gcm/fork) Now! Spread the project for great good!**
5 |
6 | mod_gcm is an ejabberd module to send offline messages as PUSH notifications for Android using Google Cloud Messaging API.
7 |
8 | > Consider using [mod_push](https://github.com/royneary/mod_push) which implements [XEP-0357](http://xmpp.org/extensions/xep-0357.html) and works with many PUSH services.
9 |
10 | This module **has nothing to do** with [XEP-0357](http://xmpp.org/extensions/xep-0357.html).
11 |
12 | The main goal of this module is to send all offline messages to the registered (see [Usage](#Usage)) clients via Google Cloud Messaging service.
13 |
14 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=Y6TWGNS5GBQ84&lc=US¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted)
15 |
16 | **Compilation**:
17 |
18 | Because of the dependencies such as xml.hrl, logger.hrl, etc it's recommended to compile the module with ejabberd itself: put it in the ejabberd/src directory and run the default compiler.
19 |
20 | **Configuration**:
21 |
22 | To let the module work fine with Google APIs, put the lines below in the ejabberd modules section:
23 | ```yaml
24 | mod_gcm:
25 | gcm_api_key: "Your Google APIs key"
26 | ```
27 | [Here](https://developer.android.com/google/gcm/gs.html) you can create your own API key for Google Cloud Messaging (you need the server key).
28 | Bear in mind that the feature is highly limited for free users.
29 |
30 | **Usage (Client to server)**:
31 |
32 | As you may know, Google Cloud Messaging **won't work** as you expect without the client part.
33 |
34 | You won't find the instructions how to create your own Google Cloud Messaging client here. Although, [this](https://developer.android.com/google/gcm/client.html) example should work fine.
35 |
36 | You also need to send this stanza to the server over the XMPP connection, to let the server know your client key:
37 | ```xml
38 |
39 |
40 | API_KEY
41 |
42 |
43 | ```
44 |
45 | The key is kept in mnesia database and completely belongs to the JabberID which it was sent from.
46 |
47 |
48 | **Compatibility**:
49 |
50 | The module works fine with Ejabberd 16 and maybe the further versions. For the older ones, checkout the *ejabberd14* branch.
51 |
--------------------------------------------------------------------------------
/mod_gcm.spec:
--------------------------------------------------------------------------------
1 | author: "John Smith "
2 | category: "service"
3 | summary: "Google Cloud Messaging for Ejabberd"
4 | home: "https://github.com/mrDoctorWho/ejabberd_mod_gcm"
5 | url: "git@github.com:mrDoctorWho/ejabberd_mod_gcm.git"
6 |
7 |
--------------------------------------------------------------------------------
/src/mod_gcm.erl:
--------------------------------------------------------------------------------
1 | %% Google Cloud Messaging for Ejabberd
2 | %% Created: 02/08/2015 by mrDoctorWho
3 | %% License: MIT/X11
4 |
5 | -module(mod_gcm).
6 | -author("mrDoctorWho").
7 |
8 | -include("ejabberd.hrl").
9 | -include("logger.hrl").
10 | -include("jlib.hrl").
11 |
12 | -behaviour(gen_mod).
13 |
14 | -record(gcm_users, {user, gcm_key, last_seen}).
15 |
16 |
17 | -define(NS_GCM, "https://android.googleapis.com/gcm"). %% I hope Google doesn't mind.
18 | -define(GCM_URL, ?NS_GCM ++ "/send").
19 | -define(CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8").
20 |
21 |
22 | -export([start/2, stop/1, message/3, iq/3, mod_opt_type/1, depends/2]).
23 |
24 | %% http://stackoverflow.com/questions/114196/url-encode-in-erlang
25 | -spec(url_encode(string()) -> string()).
26 |
27 | escape_uri(S) when is_list(S) ->
28 | escape_uri(unicode:characters_to_binary(S));
29 | escape_uri(<>) when C >= $a, C =< $z ->
30 | [C] ++ escape_uri(Cs);
31 | escape_uri(<>) when C >= $A, C =< $Z ->
32 | [C] ++ escape_uri(Cs);
33 | escape_uri(<>) when C >= $0, C =< $9 ->
34 | [C] ++ escape_uri(Cs);
35 | escape_uri(<>) when C == $. ->
36 | [C] ++ escape_uri(Cs);
37 | escape_uri(<>) when C == $- ->
38 | [C] ++ escape_uri(Cs);
39 | escape_uri(<>) when C == $_ ->
40 | [C] ++ escape_uri(Cs);
41 | escape_uri(<>) ->
42 | escape_byte(C) ++ escape_uri(Cs);
43 | escape_uri(<<>>) ->
44 | "".
45 |
46 | escape_byte(C) ->
47 | "%" ++ hex_octet(C).
48 |
49 | hex_octet(N) when N =< 9 ->
50 | [$0 + N];
51 | hex_octet(N) when N > 15 ->
52 | hex_octet(N bsr 4) ++ hex_octet(N band 15);
53 | hex_octet(N) ->
54 | [N - 10 + $a].
55 |
56 |
57 | url_encode(Data) ->
58 | url_encode(Data,"").
59 |
60 | url_encode([],Acc) ->
61 | Acc;
62 | url_encode([{Key,Value}|R],"") ->
63 | url_encode(R, escape_uri(Key) ++ "=" ++ escape_uri(Value));
64 | url_encode([{Key,Value}|R],Acc) ->
65 | url_encode(R, Acc ++ "&" ++ escape_uri(Key) ++ "=" ++ escape_uri(Value)).
66 |
67 |
68 | %% Send an HTTP request to Google APIs and handle the response
69 | send([{Key, Value}|R], API_KEY) ->
70 | Header = [{"Authorization", url_encode([{"key", API_KEY}])}],
71 | Body = url_encode([{Key, Value}|R]),
72 | {ok, RawResponse} = httpc:request(post, {?GCM_URL, Header, ?CONTENT_TYPE, Body}, [], []),
73 | %% {{"HTTP/1.1",200,"OK"} ..}
74 | {{_, SCode, Status}, ResponseBody} = {element(1, RawResponse), element(3, RawResponse)},
75 | %% TODO: Errors 5xx
76 | case SCode of
77 | 200 -> ?DEBUG("mod_gcm: t(he message was sent", []);
78 | 401 -> ?ERROR_MSG("mod_gcm: error! Code ~B (~s)", [SCode, Status]);
79 | _ -> ?ERROR_MSG("mod_gcm: error! Code ~B (~s), response: \"~s\"", [SCode, Status, ResponseBody])
80 | end.
81 |
82 |
83 | %% TODO: Define some kind of a shaper to prevent floods and the GCM API to burn out :/
84 | %% Or this could be the limits, like 10 messages/user, 10 messages/hour, etc
85 | message(From, To, Packet) ->
86 | Type = fxml:get_tag_attr_s(<<"type">>, Packet),
87 | ?DEBUG("mod_gcm: got offline message", []),
88 | case Type of
89 | "normal" -> ok;
90 | _ ->
91 | %% Strings
92 | JFrom = jlib:jid_to_string(From#jid{user = From#jid.user, server = From#jid.server, resource = <<"">>}),
93 | JTo = jlib:jid_to_string(To#jid{user = To#jid.user, server = To#jid.server, resource = <<"">>}),
94 | ToUser = To#jid.user,
95 | ToServer = To#jid.server,
96 | ServerKey = gen_mod:get_module_opt(ToServer, ?MODULE, gcm_api_key, fun(V) -> V end, undefined),
97 | Body = fxml:get_path_s(Packet, [{elem, <<"body">>}, cdata]),
98 |
99 | %% Checking subscription
100 | {Subscription, _Groups} =
101 | ejabberd_hooks:run_fold(roster_get_jid_info, ToServer, {none, []}, [ToUser, ToServer, From]),
102 | case Subscription of
103 | both ->
104 | case Body of
105 | <<>> -> ok; %% There is no body
106 | _ ->
107 | Result = mnesia:dirty_read(gcm_users, {ToUser, ToServer}),
108 | case Result of
109 | [] -> ?DEBUG("mod_gcm: no record found for ~s", [JTo]);
110 | [#gcm_users{gcm_key = API_KEY}] ->
111 | ?DEBUG("mod_gcm: sending the message to GCM for user ~s", [JTo]),
112 | Args = [{"registration_id", API_KEY}, {"data.message", Body}, {"data.source", JFrom}, {"data.destination", JTo}],
113 | if ServerKey /=
114 | undefined -> send(Args, ServerKey);
115 | true ->
116 | ?ERROR_MSG("mod_gcm: gcm_api_key is undefined!", []),
117 | ok
118 | end
119 | end
120 | end;
121 | _ -> ok
122 | end
123 | end.
124 |
125 |
126 | iq(#jid{user = User, server = Server}, _To, #iq{sub_el = SubEl} = IQ) ->
127 | LUser = jlib:nodeprep(User),
128 | LServer = jlib:nameprep(Server),
129 |
130 | {MegaSecs, Secs, _MicroSecs} = now(),
131 | TimeStamp = MegaSecs * 1000000 + Secs,
132 |
133 | API_KEY = fxml:get_tag_cdata(fxml:get_subtag(SubEl, <<"key">>)),
134 |
135 | F = fun() -> mnesia:write(#gcm_users{user={LUser, LServer}, gcm_key=API_KEY, last_seen=TimeStamp}) end,
136 |
137 | case mnesia:dirty_read(gcm_users, {LUser, LServer}) of
138 | [] ->
139 | mnesia:transaction(F),
140 | ?DEBUG("mod_gcm: new user registered ~s@~s", [LUser, LServer]);
141 |
142 | %% Record exists, the key is equal to the one we know
143 | [#gcm_users{user={LUser, LServer}, gcm_key=API_KEY}] ->
144 | mnesia:transaction(F),
145 | ?DEBUG("mod_gcm: updating last_seen for user ~s@~s", [LUser, LServer]);
146 |
147 | %% Record for this key was found, but for another key
148 | [#gcm_users{user={LUser, LServer}, gcm_key=_KEY}] ->
149 | mnesia:transaction(F),
150 | ?DEBUG("mod_gcm: updating gcm_key for user ~s@~s", [LUser, LServer])
151 | end,
152 |
153 | IQ#iq{type=result, sub_el=[]}. %% We don't need the result, but the handler have to send something.
154 |
155 |
156 | start(Host, _Opts) ->
157 | ssl:start(),
158 | application:start(inets),
159 | mnesia:create_table(gcm_users, [{disc_copies, [node()]}, {attributes, record_info(fields, gcm_users)}]),
160 | gen_iq_handler:add_iq_handler(ejabberd_local, Host, <>, ?MODULE, iq, no_queue),
161 | ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, message, 49),
162 | ?INFO_MSG("mod_gcm has started successfully!", []),
163 | ok.
164 |
165 |
166 | stop(_Host) -> ok.
167 |
168 |
169 | depends(_Host, _Opts) ->
170 | [].
171 |
172 |
173 | mod_opt_type(gcm_api_key) -> fun iolist_to_binary/1. %binary_to_list?
174 |
175 |
--------------------------------------------------------------------------------