imouto 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/imouto.rb +99 -0
- data/lib/irc.rb +111 -0
- data/lib/irc_commands.rb +50 -0
- data/lib/ratelimited_queue.rb +35 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 40af6dcddca1548d72857fde8fc7fef2f492f764
|
4
|
+
data.tar.gz: b01aef77249fdb1da5f6c92cf0f050450a575f19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c2225f2f2ae6773882ce15e17f8f21e58eb1242f45d65bbb36d98934f35cf103312c2a9bb077ff4792128deecbfb6d3705640c1a34904c8a9ecae47906acfffa
|
7
|
+
data.tar.gz: 9c177a5422eb77ac78a42d5270144309380fd4840bf06eb1775db5ecf76fb149aeccc07ef7d40536c0b29671a33dda5b0ae4612d48281fe63ac6e4097199bbcf
|
data/lib/imouto.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'irc'
|
2
|
+
require_relative 'ratelimited_queue'
|
3
|
+
|
4
|
+
module Imouto
|
5
|
+
|
6
|
+
# IRC-PRIVMSG, passed to matchers.
|
7
|
+
Message = Struct.new(
|
8
|
+
# PRIVMSG as string.
|
9
|
+
:message,
|
10
|
+
# Captures specified in matcher-regex
|
11
|
+
:captures,
|
12
|
+
# Raw MatchData
|
13
|
+
:raw
|
14
|
+
)
|
15
|
+
|
16
|
+
ImoutoConfig = Struct.new(
|
17
|
+
:loggers,
|
18
|
+
:message_interval_size,
|
19
|
+
:messages_per_interval
|
20
|
+
)
|
21
|
+
|
22
|
+
class Bot
|
23
|
+
attr_reader :matchers, :irc, :reply_queue
|
24
|
+
|
25
|
+
def initialize(irc, conf)
|
26
|
+
imouto_config = ImoutoConfig.new
|
27
|
+
conf.each { |k, v| imouto_config[k] = v; }
|
28
|
+
@loggers = imouto_config.loggers || [->(msg) { p msg }]
|
29
|
+
@matchers = {}
|
30
|
+
@irc = irc
|
31
|
+
@reply_queue = Imouto::RatelimitedQueue.new(
|
32
|
+
imouto_config.messages_per_interval || 3,
|
33
|
+
imouto_config.message_interval_size || 4)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Starts the bot, spawning a read- and a write-thread
|
37
|
+
def start
|
38
|
+
read_thread = Thread.new { read(@irc) }
|
39
|
+
write_thread = Thread.new { dequeue_replies }
|
40
|
+
read_thread.join
|
41
|
+
write_thread.join
|
42
|
+
end
|
43
|
+
|
44
|
+
# Read from the IRC-connection until it closes.
|
45
|
+
def read(irc_con = @irc)
|
46
|
+
irc_con.start.read { |msg, matchers = @matchers|
|
47
|
+
matchers.keys.each { |regex|
|
48
|
+
msg[:message] =~ regex && reply(msg, matchers[regex], regex.match(msg[:message]))
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reply to a PRIVMSG
|
54
|
+
def reply(msg, matcher, matches)
|
55
|
+
log("[Executing Matcher] #{msg[:message]}")
|
56
|
+
m = Message.new(msg[:message], matches, msg)
|
57
|
+
begin
|
58
|
+
reply = Thread.new { matcher.call(m) }.value
|
59
|
+
rescue StandardError => e
|
60
|
+
log("[Matcher Exception] #{e}")
|
61
|
+
end
|
62
|
+
reply_to = msg[:target].start_with?('#') ? msg[:target] : msg[:nick]
|
63
|
+
queue_reply(reply_to, reply)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Write replies to the IRC-connection
|
67
|
+
def dequeue_replies(irc_con = @irc)
|
68
|
+
@reply_queue.dequeue { |reply, irc = irc_con|
|
69
|
+
log("[->] #{reply['target']}: #{reply['reply']}")
|
70
|
+
irc.privmsg(reply['target'], reply['reply'])
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Enqueues a reply, to be sent.
|
75
|
+
# You can also directly send messages by using @irc.privmsg
|
76
|
+
# this however bypasses rate limiting and might get you kicked.
|
77
|
+
def queue_reply(target, reply)
|
78
|
+
@reply_queue.enqueue('target' => target, 'reply' => reply)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Calls every logger with a message.
|
82
|
+
def log(msg)
|
83
|
+
@loggers.each { |l| l.call(msg) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Registers a matcher to respond to PRIVMSGs that match a certain regex.
|
87
|
+
def register_matcher(regex, matcher)
|
88
|
+
return false unless matcher.respond_to? 'call'
|
89
|
+
@matchers[regex] = matcher
|
90
|
+
log("[Registered Matcher] #{regex}")
|
91
|
+
end
|
92
|
+
|
93
|
+
# Consumes a regex, and removes the matcher that was registered with it.
|
94
|
+
def unregister_matcher(regex)
|
95
|
+
@matchers.delete(regex)
|
96
|
+
log("[Removed Matcher] #{regex}")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/irc.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'net/protocol'
|
3
|
+
require_relative 'irc_commands'
|
4
|
+
|
5
|
+
module Imouto
|
6
|
+
Connection = Struct.new(
|
7
|
+
:server,
|
8
|
+
:port,
|
9
|
+
:channels,
|
10
|
+
:connect_timeout,
|
11
|
+
:read_timeout
|
12
|
+
)
|
13
|
+
|
14
|
+
User = Struct.new(
|
15
|
+
:nick,
|
16
|
+
:username,
|
17
|
+
:realname,
|
18
|
+
:password
|
19
|
+
)
|
20
|
+
|
21
|
+
class Enum
|
22
|
+
attr_reader :options, :selected
|
23
|
+
|
24
|
+
def initialize(options, selected)
|
25
|
+
raise ArgumentError, "Expected argument options to respond to method 'at'" unless options.respond_to? 'at'
|
26
|
+
@options = options
|
27
|
+
@selected = options.first
|
28
|
+
set selected
|
29
|
+
end
|
30
|
+
|
31
|
+
def set(value)
|
32
|
+
return @selected unless @options.include? value
|
33
|
+
@selected = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Irc
|
38
|
+
include IrcCommands
|
39
|
+
|
40
|
+
attr_reader :connection, :user, :connection_state
|
41
|
+
|
42
|
+
|
43
|
+
def initialize(connection, user)
|
44
|
+
@connection = Connection.new
|
45
|
+
connection.each { |k, v| @connection[k] = v; }
|
46
|
+
@user = User.new
|
47
|
+
user.each { |k, v| @user[k] = v; }
|
48
|
+
@connection_state = Enum.new([:not_connected, :connecting, :connected, :failed], :not_connected)
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Stuff to do when the connection is successfully established
|
53
|
+
def setup
|
54
|
+
join @connection.channels
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# see irc_commands.rb
|
59
|
+
def raw(msg)
|
60
|
+
@socket.puts msg
|
61
|
+
end
|
62
|
+
|
63
|
+
# Starts an IRC-connection. Chainable,-
|
64
|
+
# you probably want to do irc.start.read {|msg| ...}
|
65
|
+
def start
|
66
|
+
begin
|
67
|
+
Timeout.timeout(@connection.connect_timeout) {
|
68
|
+
@socket = TCPSocket.new(@connection.server, @connection.port)
|
69
|
+
raw "PASS #{@user.password}"
|
70
|
+
raw "NICK #{@user.nick}"
|
71
|
+
raw "USER #{@user.username} 0 * :#{@user.realname}"
|
72
|
+
}
|
73
|
+
rescue Timeout::Error
|
74
|
+
@connection_state.set(:failed)
|
75
|
+
puts 'Timeout! Aborting.'
|
76
|
+
return false
|
77
|
+
rescue SocketError => e
|
78
|
+
@connection_state.set(:failed)
|
79
|
+
puts "Network error: #{e}"
|
80
|
+
return false
|
81
|
+
rescue => e
|
82
|
+
@connection_state.set(:failed)
|
83
|
+
puts "General exception: #{e}"
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
@connection_state.set(:connecting)
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
# Yields PRIVMSGs and handles PINGs as well as setup operations
|
91
|
+
def read
|
92
|
+
until @socket.eof?
|
93
|
+
msg = @socket.gets
|
94
|
+
if msg.start_with? 'PING'
|
95
|
+
raw "PONG #{msg[6..-1]}"
|
96
|
+
next
|
97
|
+
end
|
98
|
+
if msg.include? 'PRIVMSG'
|
99
|
+
m = msg.chomp.match(/:(?<nick>.*)!(?<mask>.*) PRIVMSG (?<target>.*) :(?<message>.*)$/)
|
100
|
+
yield m
|
101
|
+
end
|
102
|
+
if (@connection_state.selected == :connecting) && (msg =~ /:(.*) 376 #{@user.nick} :(.*)$/)
|
103
|
+
setup
|
104
|
+
@connection_state.set(:connected)
|
105
|
+
next
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
data/lib/irc_commands.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module IrcCommands
|
2
|
+
# Overwrite raw() to send to your socket wherever you include this
|
3
|
+
def raw(string)
|
4
|
+
puts string
|
5
|
+
end
|
6
|
+
|
7
|
+
def mode(nick, state, mode)
|
8
|
+
return false unless %w(a i w r o O s).include? mode
|
9
|
+
raw "MODE #{nick} #{state}#{mode}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def quit(msg = '')
|
13
|
+
raw "QUIT :#{msg}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def join(channels)
|
17
|
+
channels.respond_to?('join') && channels = channels.join(',')
|
18
|
+
raw "JOIN #{channels}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def part(channels, msg = '')
|
22
|
+
channels.respond_to?('join') && channels = channels.join(',')
|
23
|
+
raw "PART #{channels} :#{msg}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def channel_mode(channel, state, mode, modeparams = '')
|
27
|
+
return false unless %w(O o v a i m n q p s r t k l b e I).include? mode
|
28
|
+
raw "MODE #{channel} #{state} #{mode} #{modeparams}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def topic(channel, topic)
|
32
|
+
raw "TOPIC #{channel} :#{topic}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def invite(nick, channel)
|
36
|
+
raw "INVITE #{nick} #{channel}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def kick(channel, user, reason = '')
|
40
|
+
raw "KICK #{channel} #{user} :#{reason}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def privmsg(target, msg)
|
44
|
+
raw "PRIVMSG #{target} :#{msg}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def notice(target, msg)
|
48
|
+
raw "NOTICE #{target} :#{msg}"
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Imouto
|
2
|
+
class RatelimitedQueue
|
3
|
+
attr_reader :messages_per_interval, :interval_in_seconds
|
4
|
+
|
5
|
+
def initialize(messages_per_interval, interval_in_seconds)
|
6
|
+
@messages_per_interval = messages_per_interval
|
7
|
+
@interval_in_seconds = interval_in_seconds
|
8
|
+
@messages_this_interval = 0
|
9
|
+
@current_interval = Time.now
|
10
|
+
@items = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def enqueue(item)
|
14
|
+
@items << item
|
15
|
+
end
|
16
|
+
|
17
|
+
def dequeue
|
18
|
+
loop {
|
19
|
+
if @items.empty?
|
20
|
+
sleep 1
|
21
|
+
next
|
22
|
+
end
|
23
|
+
interval = Time.now
|
24
|
+
if interval > @current_interval + @interval_in_seconds
|
25
|
+
@current_interval = interval
|
26
|
+
@messages_this_interval = 0
|
27
|
+
end
|
28
|
+
if @messages_this_interval < @messages_per_interval
|
29
|
+
@messages_this_interval += 1
|
30
|
+
yield @items.shift
|
31
|
+
end
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: imouto
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- zwwwdr
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-04 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A really basic & lightweight IRC-client-lib. Mix with a few lambdas for
|
14
|
+
instant bot.
|
15
|
+
email: zwdr@cock.li
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/imouto.rb
|
21
|
+
- lib/irc.rb
|
22
|
+
- lib/irc_commands.rb
|
23
|
+
- lib/ratelimited_queue.rb
|
24
|
+
homepage: https://github.com/2-3/imouto
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.4.5.1
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Basic IRC bot library, without frills
|
48
|
+
test_files: []
|