├── 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 | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](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 | --------------------------------------------------------------------------------