├── .gitignore ├── test └── dummy_SUITE.erl ├── src ├── ircbot.app.src ├── ircbot_sup.erl ├── ircbot_plugin_pong.erl ├── ircbot_plugin_autojoin.erl ├── ircbot.hrl ├── ircbot_plugin_ctcp.erl ├── ircbot_plugin_help.erl ├── ircbot_plugin_nickserv.erl ├── ircbot_app.erl ├── ircbot_plugin_doesnt.erl ├── ircbot_plugin_channels.erl ├── ircbot_plugins.erl ├── ircbot_plugin_uptime.erl ├── ircbot_plugin_ping.erl ├── ircbot_api.erl ├── ircbot_connection.erl ├── ircbot_lib.erl └── ircbot_fsm.erl ├── rebar.config ├── .github └── workflows │ └── ci.yml ├── settings.cfg.example ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | settings.cfg 2 | *.beam 3 | ebin/ 4 | deps/ 5 | .dialyzer_plt 6 | _build/ 7 | -------------------------------------------------------------------------------- /test/dummy_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% dummy common test suite, to satisfy the CI 2 | %%% https://erlang.org/doc/apps/common_test/basics_chapter.html 3 | 4 | -module(dummy_SUITE). 5 | -include_lib("common_test/include/ct.hrl"). 6 | -export([all/0]). 7 | -export([test/1]). 8 | all() -> [test]. 9 | 10 | test(_Config) -> 11 | 1 = 1. 12 | -------------------------------------------------------------------------------- /src/ircbot.app.src: -------------------------------------------------------------------------------- 1 | {application, ircbot, [ 2 | {description, "ircbot"}, 3 | {vsn, "0.01"}, 4 | {modules, [ 5 | ircbot_app, ircbot_sup, ircbot_fsm, ircbot_api, 6 | ircbot_lib, ircbot_plugins, 7 | pong_plugin, ctcp_plugin 8 | ]}, 9 | {mod, {ircbot_app, []}}, 10 | {applications, [kernel, stdlib, sasl]}] 11 | }. 12 | -------------------------------------------------------------------------------- /src/ircbot_sup.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_sup). 2 | -author('gdamjan@gmail.com'). 3 | 4 | -behaviour(supervisor). 5 | -export([init/1]). 6 | 7 | -define(CHILD(I), {I, {I, start_link, []}, permanent, 5000, worker, [I]}). 8 | 9 | %% supervisor behaviour 10 | init([]) -> 11 | {ok, { 12 | {simple_one_for_one, 10, 60}, 13 | [?CHILD(ircbot_fsm)] 14 | }}. 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | {deps, []}. 4 | {erl_opts, [tuple_calls]}. 5 | 6 | {profiles, [ 7 | {prod, [ 8 | {relx, [{dev_mode, false}]} 9 | ]} 10 | ]}. 11 | 12 | {relx, [ 13 | {release, {ircbot, semver}, 14 | [ircbot]}, 15 | 16 | {dev_mode, true}, 17 | {include_erts, false}, 18 | {include_src, false}, 19 | {extended_start_script, true} 20 | ]}. 21 | 22 | {shell, [{apps, []}]}. 23 | {minimum_otp_vsn, "23.0"}. 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: erlang:${{ matrix.erlang }} 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Compile 18 | run: rebar3 compile 19 | - name: Run tests 20 | run: rebar3 do eunit, ct 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | erlang: [23, 24, 25] 26 | -------------------------------------------------------------------------------- /settings.cfg.example: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | {connection, [ 4 | {name, {local, freenode}}, 5 | {nickname, "erlbot-test"}, 6 | {server, {"chat.freenode.net", 6667}}, 7 | {channels, [ 8 | "#erlbot-test" 9 | ]}, 10 | {plugins, [ 11 | {'ircbot_plugin_uptime', []}, 12 | {'ircbot_plugin_nickserv', [""]}, 13 | {'ircbot_plugin_autojoin', []}, 14 | {'ircbot_plugin_ping', []}, 15 | {'ircbot_plugin_help', ["https://github.com/gdamjan/erlang-irc-bot/"]} 16 | ]} 17 | ]}. 18 | -------------------------------------------------------------------------------- /src/ircbot_plugin_pong.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_pong). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | 8 | init(_Args) -> 9 | {ok, []}. 10 | 11 | handle_event(Msg, State) -> 12 | case Msg of 13 | {in, Ref, [<<>>,<<>>,<<"PING">>, Server]} -> 14 | Ref:pong(Server), 15 | {ok, Server}; 16 | _ -> 17 | {ok, State} 18 | end. 19 | 20 | 21 | handle_call(_Request, State) -> {ok, ok, State}. 22 | handle_info(_Info, State) -> {ok, State}. 23 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 24 | terminate(_Args, _State) -> ok. 25 | -------------------------------------------------------------------------------- /src/ircbot_plugin_autojoin.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_autojoin). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | % responds to INVITEs by joining the channel 8 | 9 | init(_Args) -> 10 | {ok, []}. 11 | 12 | handle_event(Msg, State) -> 13 | case Msg of 14 | {in, Ref, [_Sender, _User, <<"INVITE">>, _Nick, <<"#",Channel/binary>>]} -> 15 | Ref:join(<<"#",Channel/binary>>); 16 | _ -> 17 | ok 18 | end, 19 | {ok, State}. 20 | 21 | handle_call(_Request, State) -> {ok, ok, State}. 22 | handle_info(_Info, State) -> {ok, State}. 23 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 24 | terminate(_Args, _State) -> ok. 25 | -------------------------------------------------------------------------------- /src/ircbot.hrl: -------------------------------------------------------------------------------- 1 | -define(VERSION, <<"http://github.com/gdamjan/erlang-irc-bot">>). 2 | -define(REALNAME, <<"An experimental Erlang IRC bot">>). 3 | -define(QUITMSG, <<"I can feel it, my mind is going...">>). 4 | 5 | -define(SECOND, 1000). 6 | -define(MINUTE, 60 * 1000). 7 | 8 | -define(RECV_TIMEOUT, 3 * ?MINUTE). 9 | -define(SEND_TIMEOUT, 10 * ?SECOND). 10 | 11 | -define(CONNECT_TIMEOUT, 5 * ?SECOND). % wait for dns 12 | -define(REGISTER_TIMEOUT, 30 * ?SECOND). % wait for register on irc 13 | -define(RECONNECT_DELAY, 15 * ?SECOND). % fast reconnect 14 | -define(BACKOFF_DELAY, 15 * ?SECOND). % backoff reconnect 15s base, 15 | % delay of 0, 5, 20, 45, 80, 125 16 | 17 | -define(NICK_SUFFIX, <<"_">>). % append suffix to nickname if nick is in use 18 | -define(CRNL, "\r\n"). 19 | -------------------------------------------------------------------------------- /src/ircbot_plugin_ctcp.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_ctcp). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | -include("ircbot.hrl"). 8 | 9 | 10 | init(_Args) -> 11 | {ok, []}. 12 | 13 | handle_event(Msg, State) -> 14 | case Msg of 15 | {in, Ref, [Sender, _User, <<"PRIVMSG">>, _Nick, <<"\^AVERSION\^A">>]} -> 16 | Ref:notice(Sender, <<"\^AVERSION ", ?VERSION/binary, "\^A">>); 17 | {in, Ref, [Sender, _User, <<"PRIVMSG">>, _Nick, <<"\^APING ", Rest/binary>>]} -> 18 | Ref:notice(Sender, <<"\^APING ", Rest/binary>>); 19 | _ -> 20 | ok 21 | end, 22 | {ok, State}. 23 | 24 | handle_call(_Request, State) -> {ok, ok, State}. 25 | handle_info(_Info, State) -> {ok, State}. 26 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 27 | terminate(_Args, _State) -> ok. 28 | -------------------------------------------------------------------------------- /src/ircbot_plugin_help.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_help). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | -define(HELP, "help is at http://github.com/gdamjan/erlang-irc-bot/wiki/HelpOnUsage"). 8 | 9 | 10 | init([Url]) -> 11 | {ok, Url}. 12 | 13 | handle_event(Ev, Url) -> 14 | case Ev of 15 | {in, Ref, [Sender, _Name, <<"PRIVMSG">>, <<"#",Channel/binary>>, Msg]} -> 16 | case re:run(Msg, "!help", [{capture, none}]) of 17 | match -> 18 | Ref:privmsg(<<"#",Channel/binary>>, [Sender, ": help is at ", Url]); 19 | _ -> ok 20 | end; 21 | _ -> ok 22 | end, 23 | {ok, Url}. 24 | 25 | handle_call(_Request, State) -> {ok, ok, State}. 26 | handle_info(_Info, State) -> {ok, State}. 27 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 28 | terminate(_Args, _State) -> ok. 29 | -------------------------------------------------------------------------------- /src/ircbot_plugin_nickserv.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_nickserv). 2 | -behaviour(gen_event). 3 | 4 | -author("gdamjan@gmail.com"). 5 | 6 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 7 | 8 | 9 | -define(TRIGGER, <<"This nickname is registered. ", _/binary>>). 10 | 11 | %% setup the password in your settings file 12 | %% {plugins, [ 13 | %% ... 14 | %% {ircbot_plugin_nickserv, ["SECRET"]} 15 | %% ]}. 16 | 17 | init(Password) -> 18 | {ok, Password}. 19 | 20 | handle_event(Msg, Password) -> 21 | case Msg of 22 | {in, Ref, [<<"NickServ">>, _User, <<"NOTICE">>, _Nick, ?TRIGGER]} -> 23 | Ref:privmsg("NickServ", ["identify ", Password]); 24 | _ -> 25 | ok 26 | end, 27 | {ok, Password}. 28 | 29 | handle_call(_Request, State) -> {ok, ok, State}. 30 | handle_info(_Info, State) -> {ok, State}. 31 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 32 | terminate(_Args, _State) -> ok. 33 | -------------------------------------------------------------------------------- /src/ircbot_app.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_app). 2 | -author('gdamjan@gmail.com'). 3 | 4 | -behaviour(application). 5 | -export([start/2, stop/1]). 6 | 7 | -define(SUPERVISOR, ircbot_sup). 8 | 9 | %% app behaviour 10 | start(_Type, _StartArgs) -> 11 | {ok, Sup} = supervisor:start_link({local, ?SUPERVISOR}, ?SUPERVISOR, []), 12 | {ok, SettingsFile} = init:get_argument(conf), 13 | {ok, Settings} = file:consult(SettingsFile), 14 | start_all(Sup, Settings), 15 | {ok, Sup}. 16 | 17 | stop(_State) -> 18 | exit(whereis(?SUPERVISOR), shutdown). 19 | 20 | get_connections_args(Settings) -> 21 | lists:filtermap( 22 | fun(El) -> 23 | case El of 24 | {connection, Args} -> {true, Args}; 25 | _ -> false 26 | end 27 | end, Settings). 28 | 29 | start_all(Supervisor, Settings) -> 30 | lists:foreach( 31 | fun (Args) -> 32 | {ok, _Child} = supervisor:start_child(Supervisor, [Args]) 33 | end, 34 | get_connections_args(Settings) 35 | ). 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Damjan Georgievski 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/ircbot_plugin_doesnt.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_doesnt). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | 8 | 9 | init(_Args) -> 10 | {ok, []}. 11 | 12 | handle_event(Msg, State) -> 13 | case Msg of 14 | {in, Ref, [Nick, _Name, <<"PRIVMSG">>, <<"#",Channel/binary>>, Text]} -> 15 | case re:run(Text, "не работи", [unicode, caseless]) of 16 | {match, _} -> 17 | Ref:privmsg(<<"#",Channel/binary>>, [Nick, ":", 18 | " Look buddy, doesn't work is a strong statement.", 19 | " Does it sit on the couch all day? Does it want more", 20 | " money? Is it on IRC all the time? Be specific!", 21 | " Examples of what doesn't work (or the URL) tend to", 22 | " help too, or pastebin the config if that's the problem"]), 23 | {ok, State}; 24 | _ -> 25 | {ok, State} 26 | end; 27 | _ -> 28 | {ok, State} 29 | end. 30 | 31 | handle_call(_Request, State) -> {ok, ok, State}. 32 | handle_info(_Info, State) -> {ok, State}. 33 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 34 | terminate(_Args, _State) -> ok. 35 | -------------------------------------------------------------------------------- /src/ircbot_plugin_channels.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_channels). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | 8 | init(Channels) -> 9 | L = lists:map(fun(X) -> list_to_binary(X) end, Channels), 10 | State = sets:from_list(L), 11 | {ok, State}. 12 | 13 | handle_event(Msg, Channels) -> 14 | case Msg of 15 | {in, Ref, [_, _, <<"001">>, _Nick, _]} -> 16 | %% join the channels on connect 17 | lists:foreach( 18 | fun (Ch) -> Ref:join(Ch) end, 19 | sets:to_list(Channels) 20 | ), 21 | {ok, Channels}; 22 | %%{in, _Ref, [_Server, _, <<"JOIN">>, Channel]} -> 23 | %% keep track of channels 24 | %% {ok, sets:add_element(Channel, Channels)}; 25 | %%{in, _Ref, [_Server, _, <<"PART">>, Channel]} -> 26 | %% keep track of channels 27 | %% {ok, sets:del_element(Channel, Channels)}; 28 | %%{in, _Ref, [_Server, _, <<"KICK">>, Channel|_]} -> 29 | %% keep track of channels 30 | %% {ok, sets:del_element(Channel, Channels)}; 31 | _ -> 32 | {ok, Channels} 33 | end. 34 | 35 | 36 | handle_call(_Request, State) -> {ok, ok, State}. 37 | handle_info(_Info, State) -> {ok, State}. 38 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 39 | terminate(_Args, _State) -> ok. 40 | -------------------------------------------------------------------------------- /src/ircbot_plugins.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugins). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -export([start_link/1, add_handler/3, delete_handler/3, which_handlers/1, notify/2]). 5 | 6 | start_link(Settings) -> 7 | {ok, Plugins} = gen_event:start_link(), 8 | Channels = proplists:get_value(channels, Settings, []), 9 | gen_event:add_handler(Plugins, ircbot_plugin_channels, Channels), 10 | gen_event:add_handler(Plugins, ircbot_plugin_pong, []), 11 | gen_event:add_handler(Plugins, ircbot_plugin_ctcp, []), 12 | lists:foreach( 13 | fun ({Plugin, Args}) -> 14 | ok = gen_event:add_handler(Plugins, Plugin, Args) 15 | end, 16 | proplists:get_value(plugins, Settings, []) 17 | ), 18 | {ok, Plugins}. 19 | 20 | add_handler(GenEv, Plugin, Args)-> 21 | case gen_event:add_handler(GenEv, Plugin, Args) of 22 | ok -> 23 | ok; 24 | {'EXIT', Reason} -> 25 | error_logger:error_msg("Problem loading plugin ~p ~p ~n", [Plugin, Reason]); 26 | Other -> 27 | error_logger:error_msg("Loading ~p reports ~p ~n", [Plugin, Other]) 28 | end. 29 | 30 | delete_handler(GenEv, Plugin, Args)-> 31 | case gen_event:delete_handler(GenEv, Plugin, Args) of 32 | ok -> 33 | ok; 34 | {'EXIT', Reason} -> 35 | error_logger:error_msg("Problem deleting plugin ~p ~p ~n", [Plugin, Reason]); 36 | Other -> 37 | error_logger:error_msg("Deleting ~p reports ~p ~n", [Plugin, Other]) 38 | end. 39 | 40 | notify(GenEv, Msg) -> 41 | gen_event:notify(GenEv, Msg). 42 | 43 | which_handlers(GenEv) -> 44 | gen_event:which_handlers(GenEv). 45 | -------------------------------------------------------------------------------- /src/ircbot_plugin_uptime.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_uptime). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | 8 | 9 | init(_Args) -> 10 | {ok, []}. 11 | 12 | iff0(N, Suffix) -> 13 | iff0(N, <<"">>, Suffix). 14 | 15 | iff0(N, Prefix, Suffix) -> 16 | Nb = integer_to_binary(N), 17 | case N > 0 of 18 | true -> <>; 19 | false -> <<"">> 20 | end. 21 | 22 | uptime() -> 23 | {UpTime, _ } = erlang:statistics(wall_clock), 24 | {D, {H, M, S}} = calendar:seconds_to_daystime(UpTime div 1000), 25 | Days = iff0(D, <<" days, ">>), 26 | Hours = iff0(H, <<" hours, ">>), 27 | Minutes = iff0(M, <<" minutes">>), 28 | Seconds = iff0(S, <<" and ">>, <<" seconds">>), 29 | <>. 30 | 31 | memory() -> 32 | M = erlang:memory(total) / 1000 / 1000, % in MB 33 | Mb = float_to_binary(M, [{decimals, 2}]), 34 | <<"memory: ", Mb/binary, " MB">>. 35 | 36 | response() -> 37 | lists:join(" | ", [ 38 | uptime(), 39 | memory(), 40 | string:chomp(erlang:system_info(system_version)) 41 | ]). 42 | 43 | handle_event(Msg, State) -> 44 | case Msg of 45 | {in, Ref, [_Sender, _Name, <<"PRIVMSG">>, <<"#",Channel/binary>>, <<"!uptime">>]} -> 46 | Ref:privmsg(<<"#",Channel/binary>>, response()), 47 | {ok, State}; 48 | _ -> 49 | {ok, State} 50 | end. 51 | 52 | 53 | handle_call(_Request, State) -> {ok, ok, State}. 54 | handle_info(_Info, State) -> {ok, State}. 55 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 56 | terminate(_Args, _State) -> ok. 57 | -------------------------------------------------------------------------------- /src/ircbot_plugin_ping.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_plugin_ping). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_event). 5 | -export([init/1, handle_event/2, terminate/2, handle_call/2, handle_info/2, code_change/3]). 6 | 7 | 8 | init(_Args) -> 9 | SECRET_KEY = crypto:strong_rand_bytes(32), 10 | {ok, SECRET_KEY}. 11 | 12 | handle_event(Msg, SECRET_KEY) -> 13 | case Msg of 14 | {in, Ref, [_Sender, _User, <<"PRIVMSG">>, <<"#",Channel/binary>>, <<"!ping ", Who/binary>>]} -> 15 | {_, Secs, _} = os:timestamp(), 16 | Mssg = encode(Channel, Secs, SECRET_KEY), 17 | Ref:privmsg(Who, <<"\^APING ", Mssg/binary, "\^A">>); 18 | 19 | {in, Ref, [Sender, _User, <<"NOTICE">>, _Nick, <<"\^APING ", Rest/binary>>]} -> 20 | {_, Secs, _} = os:timestamp(), 21 | Mssg = strip_last_byte(Rest), 22 | {Channel, Secs_prev} = decode(Mssg, SECRET_KEY), 23 | Lag = list_to_binary(integer_to_list(Secs - Secs_prev)), 24 | Ref:privmsg(<<"#",Channel/binary>>, <>); 25 | _ -> 26 | ok 27 | end, 28 | {ok, SECRET_KEY}. 29 | 30 | 31 | decode(Bin, SECRET_KEY) -> 32 | Bin1 = base64:decode(Bin), 33 | <> = Bin1, 34 | Msg = <>, 35 | Hmac = crypto:mac(poly1305, SECRET_KEY, Msg), 36 | {Channel, Secs}. 37 | 38 | 39 | encode(Channel, Secs, SECRET_KEY) -> 40 | Msg = <>, 41 | Hmac = crypto:mac(poly1305, SECRET_KEY, Msg), 42 | base64:encode(<>). 43 | 44 | 45 | strip_last_byte(Bin) -> 46 | N = byte_size(Bin) - 1, 47 | <> = Bin, 48 | X. 49 | 50 | 51 | handle_call(_Request, State) -> {ok, ok, State}. 52 | handle_info(_Info, State) -> {ok, State}. 53 | code_change(_OldVsn, State, _Extra) -> {ok, State}. 54 | terminate(_Args, _State) -> ok. 55 | -------------------------------------------------------------------------------- /src/ircbot_api.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_api). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -export([new/1, pid/1, connect/1, disconnect/1, reconnect/1]). 5 | -export([send_event/2, send_data/2, send_message/4]). 6 | -export([privmsg/3, notice/3, join/2, part/2, ping/2, pong/2, nick/2]). 7 | -export([add_plugin/3, delete_plugin/3, which_plugins/1]). 8 | 9 | new(IrcbotRef) -> 10 | {?MODULE, IrcbotRef}. 11 | 12 | pid({?MODULE, IrcbotRef}) -> 13 | IrcbotRef. 14 | 15 | connect({?MODULE, IrcbotRef}) -> 16 | gen_fsm:send_event(IrcbotRef, connect). 17 | 18 | disconnect({?MODULE, IrcbotRef}) -> 19 | gen_fsm:sync_send_all_state_event(IrcbotRef, disconnect). 20 | 21 | reconnect({?MODULE, IrcbotRef}) -> 22 | disconnect({?MODULE, IrcbotRef}), 23 | connect({?MODULE, IrcbotRef}). 24 | 25 | 26 | add_plugin(Plugin, Args, {?MODULE, IrcbotRef}) -> 27 | gen_fsm:sync_send_all_state_event(IrcbotRef, {add_plugin, Plugin, Args}). 28 | 29 | delete_plugin(Plugin, Args, {?MODULE, IrcbotRef}) -> 30 | gen_fsm:sync_send_all_state_event(IrcbotRef, {delete_plugin, Plugin, Args}). 31 | 32 | which_plugins({?MODULE, IrcbotRef}) -> 33 | gen_fsm:sync_send_all_state_event(IrcbotRef, which_plugins). 34 | 35 | 36 | send_event(Event, {?MODULE, IrcbotRef}) -> 37 | gen_fsm:send_event(IrcbotRef, Event). 38 | 39 | send_data(Data, {?MODULE, IrcbotRef}) -> 40 | send_event({send, Data}, {?MODULE, IrcbotRef}). 41 | 42 | send_message(Cmd, Destination, Msg, {?MODULE, IrcbotRef}) -> 43 | send_data([Cmd, " ", Destination, " :", Msg], {?MODULE, IrcbotRef}). 44 | 45 | 46 | privmsg(Destination, Msg, {?MODULE, IrcbotRef}) -> 47 | send_message("PRIVMSG", Destination, Msg, {?MODULE, IrcbotRef}). 48 | 49 | notice(Destination, Msg, {?MODULE, IrcbotRef}) -> 50 | send_message("NOTICE", Destination, Msg, {?MODULE, IrcbotRef}). 51 | 52 | join(Channel, {?MODULE, IrcbotRef}) -> 53 | send_data(["JOIN ", Channel], {?MODULE, IrcbotRef}). 54 | 55 | part(Channel, {?MODULE, IrcbotRef}) -> 56 | send_data(["PART ", Channel], {?MODULE, IrcbotRef}). 57 | 58 | ping(Server, {?MODULE, IrcbotRef}) -> 59 | send_data(["PING :", Server], {?MODULE, IrcbotRef}). 60 | 61 | pong(Server, {?MODULE, IrcbotRef}) -> 62 | send_data(["PONG :", Server], {?MODULE, IrcbotRef}). 63 | 64 | nick(Nick, {?MODULE, IrcbotRef}) -> 65 | send_data(["NICK ", Nick], {?MODULE, IrcbotRef}). 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An extensible ircbot written in Erlang 2 | ====================================== 3 | 4 | It all started when I decided I need to learn Erlang. At the same time I needed 5 | a simple ircbot to handle some of the channels I frequent on. These two things 6 | put together and I decided to start this project. 7 | 8 | The evolution of my knowledge can be best seen from the history of the [git 9 | commits][commits]. 10 | 11 | [commits]: http://github.com/gdamjan/erlang-irc-bot/commits/master 12 | 13 | Now that the bot is extensible by plug-ins, and quite stable it's becoming pretty 14 | useful. It still needs improvements and is work in progress, but plug-ins can 15 | be written for anything. 16 | 17 | 18 | Patches, help and feature requests can be sent on the github issue tracker. 19 | There's a TODO list I keep there too. 20 | 21 | 22 | The bot is MIT licensed (for no particular reason), it's a very liberal license 23 | with no strings, so you can really do whatever you want with it. 24 | 25 | 26 | Quick start 27 | ----------- 28 | 29 | First, compile everything: 30 | 31 | ``` 32 | rebar3 compile 33 | ``` 34 | 35 | Second, edit and rename the `settings.cfg.example` file to `settings.cfg`. Then start 36 | an Erlang REPL shell: 37 | 38 | ``` 39 | rebar3 shell --start-clean 40 | ``` 41 | 42 | Once in the Erlang REPL you can start the bot with: 43 | 44 | ``` 45 | {ok, [{ connection, Settings }]} = file:consult("settings.cfg"). 46 | {ok, IrcBot} = ircbot_fsm:start(Settings). 47 | gen_fsm:sync_send_all_state_event(IrcBot, {add_plugin, ircbot_plugin_uptime, []}). 48 | ``` 49 | 50 | You can make changes to the source code & plugins while the bot is running. 51 | Just hit "rebar3 compile" in another terminal and then, if everything is ok, in the Erlang REPL run: 52 | ``` 53 | l(ircbot_plugin_uptime). 54 | ``` 55 | to reload the `ircbot_plugin_uptime` uptime module. 56 | 57 | or 58 | ``` 59 | l(ircbot_fsm). 60 | ``` 61 | to reload the `ircbot_fsm` module. 62 | 63 | 64 | Erlangs [code switching][code switching] and the gen_fsm/gen_event frameworks 65 | will handle all the details to run the new code without even disconnecting. 66 | 67 | [code switching]: http://en.wikipedia.org/wiki/Erlang_%28programming_language%29#Hot_code_loading_and_modules 68 | 69 | 70 | Real OTP Application 71 | -------------------- 72 | 73 | ``` 74 | rebar3 as prod release 75 | ``` 76 | 77 | An Erlang release will be in `_build/prod/rel/ircbot/`, an alternative is to get a tarball with `rebar3 as prod tar`. 78 | -------------------------------------------------------------------------------- /src/ircbot_connection.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_connection). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -include("ircbot.hrl"). 5 | 6 | -export([start_link/4, code_change/1, connect/3, connect/4]). 7 | 8 | 9 | start_link(Parent, Host, Port, Ssl) -> 10 | spawn_link(?MODULE, connect, [Parent, Host, Port, Ssl]). 11 | 12 | connect(Parent, Host, Port) -> 13 | connect(Parent, Host, Port, false). 14 | 15 | connect(Parent, Host, Port, Ssl) -> 16 | Options = [ binary, {active, true}, {packet, line}, {keepalive, true}, 17 | {send_timeout, ?SEND_TIMEOUT}], 18 | SocketType = case Ssl of 19 | true -> 20 | ssl:start(), 21 | ssl; 22 | false -> 23 | gen_tcp 24 | end, 25 | case SocketType:connect(Host, Port, Options) of 26 | {ok, Sock} -> 27 | gen_fsm:send_event(Parent, success), 28 | loop({Parent, Sock, SocketType}); 29 | {error, Reason} -> 30 | error_logger:format("gen_tcp:connect error: ~s~n", [inet:format_error(Reason)]) 31 | end, 32 | exit(die). 33 | 34 | 35 | loop({_, Sock, SocketType} = State) -> 36 | receive 37 | code_change -> 38 | ?MODULE:code_change(State); 39 | 40 | % data to send away on the socket 41 | {send, Data} -> 42 | logger:debug("OUT| ~ts", [Data]), 43 | ok = SocketType:send(Sock, [Data, ?CRNL]), 44 | loop(State); 45 | 46 | % data received - packet line makes sure it's a single line 47 | {tcp, Sock, Line} -> 48 | handle_recv_data(State, Line), 49 | loop(State); 50 | 51 | {ssl, Sock, Line} -> 52 | handle_recv_data(State, Line), 53 | loop(State); 54 | 55 | % socket closed 56 | {tcp_closed, Sock} -> 57 | handle_closed(Sock); 58 | 59 | {ssl_closed, Sock} -> 60 | handle_closed(Sock); 61 | 62 | % socket errors 63 | {tcp_error, Sock, Reason} -> 64 | handle_error(Sock, Reason); 65 | 66 | {ssl_error, Sock, Reason} -> 67 | handle_error(Sock, Reason); 68 | 69 | % close socket and quit 70 | quit -> 71 | SocketType:close(Sock) 72 | 73 | after ?RECV_TIMEOUT -> 74 | error_logger:format("No activity for more than ~b microseconds. Are we stuck?~n", [?RECV_TIMEOUT]), 75 | SocketType:close(Sock) 76 | end. 77 | 78 | handle_recv_data({Parent, _, _}, LineIn) -> 79 | Line = string:chomp(LineIn), 80 | logger:debug(" IN| ~ts", [Line]), 81 | gen_fsm:send_event(Parent, {received, Line}). 82 | 83 | 84 | handle_closed(Sock) -> 85 | error_logger:format("Socket ~w closed [~w]~n", [Sock, self()]). 86 | 87 | handle_error(Sock, Reason) -> 88 | error_logger:format("Socket ~w error: ~w [~w]~n", [Sock, Reason, self()]). 89 | 90 | code_change(State) -> loop(State). 91 | -------------------------------------------------------------------------------- /src/ircbot_lib.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_lib). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -export([irc_parse/1, url_match/1, url_match/2, escape_uri/1]). 5 | -export([iolist_join/1, iolist_join/2, sanitize_utf8/1]). 6 | 7 | %% Based on http://regexlib.com/RETester.aspx?regexp_id=1057 8 | url_match(Line, Suffix) -> 9 | Re = "(((http|https)://)|(www\\.))?" 10 | "(([a-zA-Z0-9\\._-]+\\.[a-zA-Z]{2,6})|" 11 | "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}))" 12 | "(/[a-zA-Z0-9\\&%_\\./-~-]*)?" ++ "(" ++ Suffix ++ ")", 13 | re:run(Line, Re, [caseless, {capture, [0], binary}]). 14 | 15 | url_match(Line) -> 16 | url_match(Line, ""). 17 | 18 | %% Stolen from mochiweb and stackoverflow and erlang otp sources 19 | -define(FULLSTOP, 46). % $\. 20 | -define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse 21 | (C >= $A andalso C =< $Z) orelse 22 | (C >= $0 andalso C =< $9) orelse 23 | (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse 24 | C =:= $_))). 25 | 26 | hexdigit(C) when C < 10 -> $0 + C; 27 | hexdigit(C) when C < 16 -> $A + (C - 10). 28 | 29 | escape_byte(C) when ?QS_SAFE(C) -> <>; 30 | escape_byte(C) -> 31 | <> = <>, 32 | Hi1 = <<(hexdigit(Hi))>>, 33 | Lo1 = <<(hexdigit(Lo))>>, 34 | <<"%", Hi1/binary, Lo1/binary>>. 35 | 36 | %% returns binary 37 | escape_uri(S) when is_list(S) -> 38 | escape_uri(unicode:characters_to_binary(S)); 39 | escape_uri(Bin) -> 40 | << <<(escape_byte(X))/binary>> || <> <= Bin >>. 41 | 42 | 43 | %%% Erlang IRC message parsing made for parsing binaries 44 | %%% http://www.irchelp.org/irchelp/rfc/rfc2812.txt 45 | 46 | % if a string (a list of chars) is supplied, convert to a binary 47 | % this is only usefull while testing, real IRC data will always be binary 48 | % NOTE: optionally in R13 you could use unicode:characters_to_binary 49 | irc_parse(Line) when is_list(Line) -> 50 | irc_parse(list_to_binary(Line)); 51 | 52 | 53 | % Line begins with a ":", first parse the prefix 54 | % everything else is the Command 55 | irc_parse(<<":", Line/binary>>) -> 56 | [Prefix | Rest] = re:split(Line, " ", [{parts,2}]), 57 | [Nick | User] = re:split(Prefix, "[!@]", [{parts,2}]), 58 | parse_command(Rest, [Nick, User]); 59 | 60 | % there's no prefix (no servername, Nick or User) 61 | % go to parsing the Command 62 | irc_parse(Line) -> 63 | parse_command(Line, [<<>>,<<>>]). 64 | 65 | % first checks for " :", everything after that will be the Trailing 66 | % if there's no Trailing, just split 16 parts (including the Command) 67 | % if there's is Trailing, split 15 parts 68 | parse_command(Line, Acc) -> 69 | [Front | Trailing] = re:split(Line, " :", [{parts, 2}]), 70 | Parts = if length(Trailing) == 0 -> 16; true -> 15 end, 71 | [Command | Params] = re:split(Front, " ", [{parts, Parts}]), 72 | {match, Acc ++ [Command] ++ Params ++ Trailing}. 73 | 74 | 75 | % 76 | % Similar to string:join but doesn't require strings, and 77 | % returns an iolist 78 | % 79 | iolist_join([B], _, Acc) -> 80 | lists:reverse([ B | Acc ]); 81 | 82 | iolist_join([B|T], Sep, Acc) -> 83 | iolist_join(T, Sep, [ [B, Sep] | Acc ]). 84 | 85 | iolist_join(L, Sep) -> 86 | iolist_join(L, Sep, []). 87 | 88 | iolist_join(L) -> 89 | iolist_join(L, " "). 90 | 91 | % 92 | % make a best effort to get a proper utf8 binary out of a the input binary 93 | % 94 | sanitize_utf8(Bin) -> 95 | case unicode:characters_to_list(Bin) of 96 | {incomplete, Encoded, _Rest} -> 97 | unicode:characters_to_binary(Encoded); 98 | {error, Encoded, _Rest} -> 99 | % should improve the heuristics when to decode as latin1 100 | case Encoded of 101 | <<>> -> 102 | unicode:characters_to_binary(unicode:characters_to_list(Bin, latin1)); 103 | _ -> 104 | unicode:characters_to_binary(Encoded) 105 | end; 106 | List -> 107 | unicode:characters_to_binary(List) 108 | end. 109 | -------------------------------------------------------------------------------- /src/ircbot_fsm.erl: -------------------------------------------------------------------------------- 1 | -module(ircbot_fsm). 2 | -author("gdamjan@gmail.com"). 3 | 4 | -behaviour(gen_fsm). 5 | 6 | %%% public api 7 | -export([new/1, start/1, new_link/1, start_link/1]). 8 | 9 | %% gen_fsm callbacks 10 | -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, 11 | terminate/3, code_change/4]). 12 | %% states 13 | -export([standby/2, ready/2, connecting/2, registering/2]). 14 | 15 | -include("ircbot.hrl"). 16 | -record(state, { 17 | nickname, 18 | password, 19 | server, 20 | plugins, 21 | connection, 22 | backoff, 23 | timer, 24 | ssl 25 | }). 26 | 27 | 28 | %% PM api to use in the shell 29 | new(Settings) -> 30 | {ok, Ref} = start(Settings), 31 | ircbot_api:new(Ref). 32 | 33 | new_link(Settings) -> 34 | {ok, Ref} = start_link(Settings), 35 | ircbot_api:new(Ref). 36 | 37 | %% traditional OTP api 38 | start(Settings) -> 39 | case proplists:get_value(name, Settings) of 40 | undefined -> 41 | gen_fsm:start(?MODULE, Settings, []); 42 | FsmName -> 43 | gen_fsm:start(FsmName, ?MODULE, Settings, []) 44 | end. 45 | 46 | start_link(Settings) -> 47 | case proplists:get_value(name, Settings) of 48 | undefined -> 49 | gen_fsm:start_link(?MODULE, Settings, []); 50 | FsmName -> 51 | gen_fsm:start_link(FsmName, ?MODULE, Settings, []) 52 | end. 53 | 54 | 55 | %%% gen_fsm init/1 56 | %%% `Settings` should be a proplist ussually created from a 57 | %%% config file with file:consult 58 | init(Settings) -> 59 | case proplists:get_value(ssl, Settings) of 60 | true -> 61 | Ssl = true; 62 | _ -> 63 | Ssl = false 64 | end, 65 | Nick = proplists:get_value(nickname, Settings), 66 | Password = proplists:get_value(password, Settings), 67 | Server = proplists:get_value(server, Settings), 68 | {ok, Plugins} = ircbot_plugins:start_link(Settings), 69 | StateData = #state{nickname=Nick,password=Password,server=Server,plugins=Plugins,backoff=0,ssl=Ssl}, 70 | gen_fsm:send_event(self(), connect), 71 | {ok, standby, StateData}. 72 | 73 | 74 | %%% helpers 75 | send(Conn, Data) -> 76 | Conn ! {send, Data}. 77 | 78 | send_quit(Conn) -> 79 | send(Conn, ["QUIT :", ?QUITMSG]), 80 | Conn ! quit. 81 | 82 | send_login(Conn, Nickname, undefined) -> 83 | send(Conn, ["NICK ", Nickname]), 84 | send(Conn, ["USER ", Nickname, " 8 * :", ?REALNAME]); 85 | 86 | send_login(Conn, Nickname, Password) -> 87 | send(Conn, ["PASS ", Password]), 88 | send(Conn, ["NICK ", Nickname]), 89 | send(Conn, ["USER ", Nickname, " 8 * :", ?REALNAME]). 90 | 91 | %%% 92 | %%% gen_fsm states 93 | %%% 94 | standby(connect, StateData) -> 95 | {Host, Port} = StateData#state.server, 96 | Ssl = StateData#state.ssl, 97 | Pid = ircbot_connection:start_link(self(), Host, Port, Ssl), 98 | NewStateData = StateData#state{connection=Pid}, 99 | io:format("connect in standby -> connecting~n"), 100 | {next_state, connecting, NewStateData, ?CONNECT_TIMEOUT}; 101 | 102 | standby({reconnect, How}, StateData) -> 103 | case How of 104 | fast -> 105 | Delay = ?RECONNECT_DELAY, 106 | Backoff = 0; 107 | backoff -> 108 | Backoff = if 109 | StateData#state.backoff >= 5 -> 5; 110 | true -> StateData#state.backoff + 1 111 | end, 112 | % some kind of quadratic backoff 113 | Delay = Backoff * Backoff * ?BACKOFF_DELAY + ?RECONNECT_DELAY 114 | end, 115 | io:format("reconnect in ~p seconds~n", [Delay/1000]), 116 | Ref = gen_fsm:send_event_after(Delay, connect), 117 | NewStateData = StateData#state{backoff=Backoff,timer=Ref}, 118 | {next_state, standby, NewStateData}; 119 | 120 | standby(_Ev, StateData) -> 121 | {next_state, standby, StateData}. 122 | 123 | 124 | connecting(timeout, StateData) -> 125 | gen_fsm:send_event_after(0, {reconnect, fast}), 126 | Pid = StateData#state.connection, 127 | erlang:exit(Pid, kill), 128 | io:format("timeout in connecting -> standby~n"), 129 | {next_state, standby, StateData}; 130 | 131 | connecting(exit, StateData) -> 132 | gen_fsm:send_event_after(0, {reconnect, backoff}), 133 | io:format("connection died in connecting -> standby~n"), 134 | {next_state, standby, StateData}; 135 | 136 | connecting(success, StateData) -> 137 | Pid = StateData#state.connection, 138 | Nick = StateData#state.nickname, 139 | Password = StateData#state.password, 140 | send_login(Pid, Nick, Password), 141 | NewStateData = StateData#state{backoff=0}, 142 | io:format("success in connecting -> registering~n"), 143 | {next_state, registering, NewStateData, ?REGISTER_TIMEOUT}; 144 | 145 | connecting(_Ev, StateData) -> 146 | {next_state, connecting, StateData, ?CONNECT_TIMEOUT}. 147 | 148 | 149 | 150 | registering(timeout, StateData) -> 151 | Pid = StateData#state.connection, 152 | erlang:exit(Pid, kill), 153 | gen_fsm:send_event_after(0, {reconnect, backoff}), 154 | io:format("timeout: register -> standby~n"), 155 | {next_state, standby, StateData}; 156 | 157 | registering(exit, StateData) -> 158 | gen_fsm:send_event_after(0, {reconnect, backoff}), 159 | io:format("connection died: register -> standby~n"), 160 | {next_state, standby, StateData}; 161 | 162 | registering({received, Msg}, StateData) -> 163 | {match, IrcMessage} = ircbot_lib:irc_parse(Msg), 164 | case IrcMessage of 165 | [_, _, <<"001">>, _, _] -> 166 | Self = ircbot_api:new(self()), 167 | Plugins = StateData#state.plugins, 168 | ircbot_plugins:notify(Plugins, {Self, online}), 169 | ircbot_plugins:notify(Plugins, {in, Self, IrcMessage}), 170 | {next_state, ready, StateData}; 171 | [_, _, <<"433">>, <<"*">>, <>, _] -> 172 | ChangeNick = [<<"NICK ">>, Nick, ?NICK_SUFFIX], 173 | send(StateData#state.connection, ChangeNick), 174 | {next_state, registering, StateData, ?REGISTER_TIMEOUT}; 175 | [_, _, <<"PING">>, Ping] -> 176 | Pong = [<<"PONG :", Ping/binary>>], 177 | send(StateData#state.connection, Pong), 178 | {next_state, registering, StateData, ?REGISTER_TIMEOUT}; 179 | _ -> 180 | {next_state, registering, StateData, ?REGISTER_TIMEOUT} 181 | end; 182 | 183 | registering(_Ev, StateData) -> 184 | {next_state, registering, StateData, ?REGISTER_TIMEOUT}. 185 | 186 | 187 | 188 | ready({send, Msg}, StateData) -> 189 | send(StateData#state.connection, Msg), 190 | {next_state, ready, StateData}; 191 | 192 | ready({received, Msg}, StateData) -> 193 | {match, IrcMessage} = ircbot_lib:irc_parse(Msg), 194 | Self = ircbot_api:new(self()), 195 | Plugins = StateData#state.plugins, 196 | ircbot_plugins:notify(Plugins, {in, Self, IrcMessage}), % notify all plugins 197 | {next_state, ready, StateData}; 198 | 199 | 200 | ready(_Ev, StateData) -> 201 | {next_state, ready, StateData}. 202 | 203 | 204 | handle_info({'EXIT', Pid, Reason}, StateName, StateData) -> 205 | % log Pid and Reason? 206 | io:format("Pid: ~p EXITed in state: ~p for reason: ~p~n", [Pid, StateName, Reason]), 207 | {stop, die, StateData}; 208 | 209 | 210 | handle_info(Info, StateName, StateData) -> 211 | %%% if StateName is connecting or registering should return a timeout 212 | io:format("BAD: ~p ~p~n", [Info, StateName]), 213 | {next_state, StateName, StateData}. 214 | 215 | handle_event(_Ev, _StateName, _StateData) -> 216 | {stop, "Should never happen! Please don't use gen_fsm:send_all_state_event"}. 217 | 218 | 219 | handle_sync_event(disconnect, _From, StateName, StateData) -> 220 | io:format("disconnect: ~p -> standby~n", [StateName]), 221 | Ref = StateData#state.timer, 222 | if 223 | is_reference(Ref) -> gen_fsm:cancel_timer(Ref); 224 | true -> ok 225 | end, 226 | Pid = StateData#state.connection, 227 | if 228 | is_pid(Pid) -> send_quit(Pid); 229 | true -> ok 230 | end, 231 | NewStateData = StateData#state{backoff=0,timer=undefined}, 232 | {reply, ok, standby, NewStateData}; 233 | 234 | %% Plugin managemenet 235 | handle_sync_event({add_plugin, Plugin, Args}, _From, StateName, StateData) -> 236 | Plugins = StateData#state.plugins, 237 | ircbot_plugins:add_handler(Plugins, Plugin, Args), 238 | {reply, ok, StateName, StateData}; 239 | 240 | handle_sync_event({delete_plugin, Plugin, Args}, _From, StateName, StateData) -> 241 | Plugins = StateData#state.plugins, 242 | ircbot_plugins:delete_handler(Plugins, Plugin, Args), 243 | {reply, ok, StateName, StateData}; 244 | 245 | handle_sync_event(which_plugins, _From, StateName, StateData) -> 246 | Plugins = StateData#state.plugins, 247 | Reply = ircbot_plugins:which_handlers(Plugins), 248 | {reply, Reply, StateName, StateData}; 249 | 250 | handle_sync_event(_Ev, _From, _StateName, _StateData) -> 251 | {stop, "Should never happen! Please don't use gen_fsm:sync_send_all_state_event"}. 252 | 253 | 254 | %% OTP code_change and terminate 255 | code_change(_OldVsn, StateName, StateData, _Extra) -> 256 | Pid = StateData#state.connection, 257 | if 258 | is_pid(Pid) -> Pid ! code_change; 259 | true -> ok 260 | end, 261 | {ok, StateName, StateData}. 262 | 263 | terminate(_Reason, _StateName, _StateData) -> ok. 264 | --------------------------------------------------------------------------------