ircinch 2.4.0
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/.standard.yml +3 -0
- data/CHANGELOG.md +298 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +23 -0
- data/README.md +195 -0
- data/Rakefile +14 -0
- data/docs/bot_options.md +454 -0
- data/docs/changes.md +541 -0
- data/docs/common_mistakes.md +60 -0
- data/docs/common_tasks.md +57 -0
- data/docs/encodings.md +69 -0
- data/docs/events.md +273 -0
- data/docs/getting_started.md +184 -0
- data/docs/logging.md +90 -0
- data/docs/migrating.md +267 -0
- data/docs/plugins.md +4 -0
- data/docs/readme.md +20 -0
- data/examples/basic/autovoice.rb +32 -0
- data/examples/basic/google.rb +35 -0
- data/examples/basic/hello.rb +14 -0
- data/examples/basic/join_part.rb +35 -0
- data/examples/basic/memo.rb +39 -0
- data/examples/basic/msg.rb +15 -0
- data/examples/basic/seen.rb +37 -0
- data/examples/basic/urban_dict.rb +36 -0
- data/examples/basic/url_shorten.rb +36 -0
- data/examples/plugins/autovoice.rb +37 -0
- data/examples/plugins/custom_prefix.rb +22 -0
- data/examples/plugins/dice_roll.rb +38 -0
- data/examples/plugins/google.rb +36 -0
- data/examples/plugins/hello.rb +21 -0
- data/examples/plugins/hooks.rb +34 -0
- data/examples/plugins/join_part.rb +41 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/msg.rb +21 -0
- data/examples/plugins/multiple_matches.rb +32 -0
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/seen.rb +44 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +34 -0
- data/ircinch.gemspec +43 -0
- data/lib/cinch/ban.rb +53 -0
- data/lib/cinch/bot.rb +476 -0
- data/lib/cinch/cached_list.rb +21 -0
- data/lib/cinch/callback.rb +22 -0
- data/lib/cinch/channel.rb +465 -0
- data/lib/cinch/channel_list.rb +31 -0
- data/lib/cinch/configuration/bot.rb +50 -0
- data/lib/cinch/configuration/dcc.rb +18 -0
- data/lib/cinch/configuration/plugins.rb +43 -0
- data/lib/cinch/configuration/sasl.rb +21 -0
- data/lib/cinch/configuration/ssl.rb +21 -0
- data/lib/cinch/configuration/timeouts.rb +16 -0
- data/lib/cinch/configuration.rb +75 -0
- data/lib/cinch/constants.rb +535 -0
- data/lib/cinch/dcc/dccable_object.rb +39 -0
- data/lib/cinch/dcc/incoming/send.rb +149 -0
- data/lib/cinch/dcc/incoming.rb +3 -0
- data/lib/cinch/dcc/outgoing/send.rb +123 -0
- data/lib/cinch/dcc/outgoing.rb +3 -0
- data/lib/cinch/dcc.rb +14 -0
- data/lib/cinch/exceptions.rb +48 -0
- data/lib/cinch/formatting.rb +127 -0
- data/lib/cinch/handler.rb +120 -0
- data/lib/cinch/handler_list.rb +92 -0
- data/lib/cinch/helpers.rb +230 -0
- data/lib/cinch/i_support.rb +100 -0
- data/lib/cinch/irc.rb +924 -0
- data/lib/cinch/log_filter.rb +23 -0
- data/lib/cinch/logger/formatted_logger.rb +100 -0
- data/lib/cinch/logger/zcbot_logger.rb +26 -0
- data/lib/cinch/logger.rb +171 -0
- data/lib/cinch/logger_list.rb +88 -0
- data/lib/cinch/mask.rb +69 -0
- data/lib/cinch/message.rb +397 -0
- data/lib/cinch/message_queue.rb +104 -0
- data/lib/cinch/mode_parser.rb +78 -0
- data/lib/cinch/network.rb +106 -0
- data/lib/cinch/open_ended_queue.rb +26 -0
- data/lib/cinch/pattern.rb +66 -0
- data/lib/cinch/plugin.rb +517 -0
- data/lib/cinch/plugin_list.rb +40 -0
- data/lib/cinch/rubyext/float.rb +5 -0
- data/lib/cinch/rubyext/module.rb +28 -0
- data/lib/cinch/rubyext/string.rb +35 -0
- data/lib/cinch/sasl/dh_blowfish.rb +73 -0
- data/lib/cinch/sasl/diffie_hellman.rb +50 -0
- data/lib/cinch/sasl/mechanism.rb +8 -0
- data/lib/cinch/sasl/plain.rb +29 -0
- data/lib/cinch/sasl.rb +36 -0
- data/lib/cinch/syncable.rb +83 -0
- data/lib/cinch/target.rb +199 -0
- data/lib/cinch/timer.rb +147 -0
- data/lib/cinch/user.rb +489 -0
- data/lib/cinch/user_list.rb +89 -0
- data/lib/cinch/utilities/deprecation.rb +18 -0
- data/lib/cinch/utilities/encoding.rb +39 -0
- data/lib/cinch/utilities/kernel.rb +15 -0
- data/lib/cinch/version.rb +6 -0
- data/lib/cinch.rb +7 -0
- data/lib/ircinch.rb +7 -0
- metadata +205 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "openssl"
|
5
|
+
|
6
|
+
require_relative "mechanism"
|
7
|
+
|
8
|
+
module Cinch
|
9
|
+
module SASL
|
10
|
+
# DH-BLOWFISH is a combination of Diffie-Hellman key exchange and
|
11
|
+
# the Blowfish encryption algorithm. Due to its nature it is more
|
12
|
+
# secure than transmitting the password unencrypted and can be
|
13
|
+
# used on potentially insecure networks.
|
14
|
+
class DhBlowfish < Mechanism
|
15
|
+
class << self
|
16
|
+
# @return [String]
|
17
|
+
def mechanism_name
|
18
|
+
"DH-BLOWFISH"
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Array(Numeric, Numeric, Numeric)] p, g and y for DH
|
22
|
+
def unpack_payload(payload)
|
23
|
+
pgy = []
|
24
|
+
payload = payload.dup
|
25
|
+
|
26
|
+
3.times do
|
27
|
+
size = payload.unpack1("n")
|
28
|
+
payload.slice!(0, 2)
|
29
|
+
pgy << payload.unpack1("a#{size}")
|
30
|
+
payload.slice!(0, size)
|
31
|
+
end
|
32
|
+
|
33
|
+
pgy.map { |i| OpenSSL::BN.new(i, 2).to_i }
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [String] user
|
37
|
+
# @param [String] password
|
38
|
+
# @param [String] payload
|
39
|
+
# @return [String]
|
40
|
+
def generate(user, password, payload)
|
41
|
+
# duplicate the passed strings because we are modifying them
|
42
|
+
# later and they might come from the configuration store or
|
43
|
+
# similar
|
44
|
+
user = user.dup
|
45
|
+
password = password.dup
|
46
|
+
|
47
|
+
data = Base64.decode64(payload).force_encoding("ASCII-8BIT")
|
48
|
+
|
49
|
+
p, g, y = unpack_payload(data)
|
50
|
+
|
51
|
+
dh = DiffieHellman.new(p, g, 23)
|
52
|
+
pub_key = dh.generate
|
53
|
+
secret = OpenSSL::BN.new(dh.secret(y).to_s).to_s(2)
|
54
|
+
public = OpenSSL::BN.new(pub_key.to_s).to_s(2)
|
55
|
+
|
56
|
+
# Pad password so its length is a multiple of the cipher block size
|
57
|
+
password << "\0"
|
58
|
+
password << "." * (8 - (password.size % 8))
|
59
|
+
|
60
|
+
cipher = OpenSSL::Cipher.new("BF-ECB")
|
61
|
+
cipher.key_len = 32 # OpenSSL's default of 16 doesn't work
|
62
|
+
cipher.encrypt
|
63
|
+
cipher.key = secret
|
64
|
+
|
65
|
+
crypted = cipher.update(password) # we do not want the content of cipher.final
|
66
|
+
|
67
|
+
answer = [public.bytesize, public, user, crypted].pack("na*Z*a*")
|
68
|
+
Base64.strict_encode64(answer)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
module SASL
|
5
|
+
class DiffieHellman
|
6
|
+
attr_reader :p, :g, :q, :x, :e
|
7
|
+
|
8
|
+
def initialize(p, g, q)
|
9
|
+
@p = p
|
10
|
+
@g = g
|
11
|
+
@q = q
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate(tries = 16)
|
15
|
+
tries.times do
|
16
|
+
@x = rand(@q)
|
17
|
+
@e = mod_exp(@g, @x, @p)
|
18
|
+
return @e if valid?
|
19
|
+
end
|
20
|
+
raise ArgumentError, "can't generate valid e"
|
21
|
+
end
|
22
|
+
|
23
|
+
# compute the shared secret, given the public key
|
24
|
+
def secret(f)
|
25
|
+
mod_exp(f, @x, @p)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# validate a public key
|
31
|
+
def valid?
|
32
|
+
@e&.between?(2, @p - 2) && bits_set(@e) > 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def bits_set(e)
|
36
|
+
("%b" % e).count("1")
|
37
|
+
end
|
38
|
+
|
39
|
+
def mod_exp(b, e, m)
|
40
|
+
result = 1
|
41
|
+
while e > 0
|
42
|
+
result = (result * b) % m if e[0] == 1
|
43
|
+
e = e >> 1
|
44
|
+
b = (b * b) % m
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
require_relative "mechanism"
|
6
|
+
|
7
|
+
module Cinch
|
8
|
+
module SASL
|
9
|
+
# The simplest mechanisms simply transmits the username and
|
10
|
+
# password without adding any encryption or hashing. As such it's more
|
11
|
+
# insecure than DH-BLOWFISH and should only be used in combination with
|
12
|
+
# SSL.
|
13
|
+
class Plain < Mechanism
|
14
|
+
class << self
|
15
|
+
# @return [String]
|
16
|
+
def mechanism_name
|
17
|
+
"PLAIN"
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [String] user
|
21
|
+
# @param [String] password
|
22
|
+
# @return [String]
|
23
|
+
def generate(user, password, _ = nil)
|
24
|
+
Base64.strict_encode64([user, user, password].join("\0"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/cinch/sasl.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sasl/diffie_hellman"
|
4
|
+
require_relative "sasl/plain"
|
5
|
+
require_relative "sasl/dh_blowfish"
|
6
|
+
|
7
|
+
module Cinch
|
8
|
+
# SASL is a modern way of authentication in IRC, solving problems
|
9
|
+
# such as transmitting passwords as plain text (see the DH-BLOWFISH
|
10
|
+
# mechanism) and fully identifying before joining any channels.
|
11
|
+
#
|
12
|
+
# Cinch automatically detects which mechanisms are supported by the
|
13
|
+
# IRC network and uses the best available one.
|
14
|
+
#
|
15
|
+
# # Supported Mechanisms
|
16
|
+
#
|
17
|
+
# - {SASL::DhBlowfish DH-BLOWFISH}
|
18
|
+
# - {SASL::Plain PLAIN}
|
19
|
+
#
|
20
|
+
# # Configuration
|
21
|
+
# In order to use SASL one has to set the username and password
|
22
|
+
# options as follows:
|
23
|
+
#
|
24
|
+
# configure do |c|
|
25
|
+
# c.sasl.username = "foo"
|
26
|
+
# c.sasl.password = "bar"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @note All classes and modules in this module are for internal use by
|
30
|
+
# Cinch only.
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
# @since 2.0.0
|
34
|
+
module SASL
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
# Provide blocking access to user/channel information.
|
5
|
+
module Syncable
|
6
|
+
# Blocks until the object is synced.
|
7
|
+
#
|
8
|
+
# @return [void]
|
9
|
+
# @api private
|
10
|
+
def wait_until_synced(attr)
|
11
|
+
attr = attr.to_sym
|
12
|
+
waited = 0
|
13
|
+
loop do
|
14
|
+
return if attribute_synced?(attr)
|
15
|
+
waited += 1
|
16
|
+
|
17
|
+
if waited % 100 == 0
|
18
|
+
bot.loggers.warn "A synced attribute ('%s' for %s) has not been available for %d seconds, still waiting" % [attr, inspect, waited / 10]
|
19
|
+
bot.loggers.warn caller.map { |s| " #{s}" }
|
20
|
+
|
21
|
+
if waited / 10 >= 30
|
22
|
+
bot.loggers.warn " Giving up..."
|
23
|
+
raise Exceptions::SyncedAttributeNotAvailable, "'%s' for %s" % [attr, inspect]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
sleep 0.1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
# @return [void]
|
32
|
+
def sync(attribute, value, data = false)
|
33
|
+
if data
|
34
|
+
@data[attribute] = value
|
35
|
+
else
|
36
|
+
instance_variable_set("@#{attribute}", value)
|
37
|
+
end
|
38
|
+
@synced_attributes << attribute
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Boolean]
|
42
|
+
# @api private
|
43
|
+
def attribute_synced?(attribute)
|
44
|
+
@synced_attributes.include?(attribute)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [void]
|
48
|
+
# @api private
|
49
|
+
def unsync(attribute)
|
50
|
+
@synced_attributes.delete(attribute)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [void]
|
54
|
+
# @api private
|
55
|
+
# @since 1.0.1
|
56
|
+
def unsync_all
|
57
|
+
@synced_attributes.clear
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [Symbol] attribute
|
61
|
+
# @param [Boolean] data
|
62
|
+
# @param [Boolean] unsync
|
63
|
+
# @api private
|
64
|
+
def attr(attribute, data = false, unsync = false)
|
65
|
+
unless unsync
|
66
|
+
@when_requesting_synced_attribute&.call(attribute)
|
67
|
+
wait_until_synced(attribute)
|
68
|
+
end
|
69
|
+
|
70
|
+
if data
|
71
|
+
@data[attribute]
|
72
|
+
else
|
73
|
+
instance_variable_get("@#{attribute}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @api private
|
78
|
+
# @return [void]
|
79
|
+
def mark_as_synced(attribute)
|
80
|
+
@synced_attributes << attribute
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/cinch/target.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cinch
|
4
|
+
# @since 2.0.0
|
5
|
+
class Target
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :name
|
10
|
+
# @return [Bot]
|
11
|
+
attr_reader :bot
|
12
|
+
def initialize(name, bot)
|
13
|
+
@name = name
|
14
|
+
@bot = bot
|
15
|
+
end
|
16
|
+
|
17
|
+
# Sends a NOTICE to the target.
|
18
|
+
#
|
19
|
+
# @param [#to_s] text the message to send
|
20
|
+
# @return [void]
|
21
|
+
# @see #safe_notice
|
22
|
+
def notice(text)
|
23
|
+
send(text, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sends a PRIVMSG to the target.
|
27
|
+
#
|
28
|
+
# @param [#to_s] text the message to send
|
29
|
+
# @param [Boolean] notice Use NOTICE instead of PRIVMSG?
|
30
|
+
# @return [void]
|
31
|
+
# @see #safe_msg
|
32
|
+
# @note The aliases `msg` and `privmsg` are deprecated and will be
|
33
|
+
# removed in a future version.
|
34
|
+
def send(text, notice = false)
|
35
|
+
# TODO deprecate `notice` argument, put splitting into own
|
36
|
+
# method
|
37
|
+
text = text.to_s
|
38
|
+
split_start = @bot.config.message_split_start || ""
|
39
|
+
split_end = @bot.config.message_split_end || ""
|
40
|
+
command = notice ? "NOTICE" : "PRIVMSG"
|
41
|
+
prefix = ":#{@bot.mask} #{command} #{@name} :"
|
42
|
+
|
43
|
+
text.lines.map(&:chomp).each do |line|
|
44
|
+
splitted = split_message(line, prefix, split_start, split_end)
|
45
|
+
|
46
|
+
splitted[0, (@bot.config.max_messages || splitted.size)].each do |string|
|
47
|
+
@bot.irc.send("#{command} #{@name} :#{string}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
alias_method :msg, :send # deprecated
|
52
|
+
alias_method :privmsg, :send # deprecated
|
53
|
+
undef_method(:msg) # yardoc hack
|
54
|
+
undef_method(:privmsg) # yardoc hack
|
55
|
+
|
56
|
+
# @deprecated
|
57
|
+
def msg(*args)
|
58
|
+
Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Target#msg", "Target#send")
|
59
|
+
send(*args)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @deprecated
|
63
|
+
def privmsg(*args)
|
64
|
+
Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Target#privmsg", "Target#send")
|
65
|
+
send(*args)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Like {#send}, but remove any non-printable characters from
|
69
|
+
# `text`. The purpose of this method is to send text of untrusted
|
70
|
+
# sources, like other users or feeds.
|
71
|
+
#
|
72
|
+
# Note: this will **break** any mIRC color codes embedded in the
|
73
|
+
# string. For more fine-grained control, use
|
74
|
+
# {Helpers#Sanitize} and
|
75
|
+
# {Formatting.unformat} directly.
|
76
|
+
#
|
77
|
+
# @return (see #send)
|
78
|
+
# @param (see #send)
|
79
|
+
# @see #send
|
80
|
+
def safe_send(text, notice = false)
|
81
|
+
send(Cinch::Helpers.sanitize(text), notice)
|
82
|
+
end
|
83
|
+
alias_method :safe_msg, :safe_send # deprecated
|
84
|
+
alias_method :safe_privmsg, :safe_msg # deprecated
|
85
|
+
undef_method(:safe_msg) # yardoc hack
|
86
|
+
undef_method(:safe_privmsg) # yardoc hack
|
87
|
+
|
88
|
+
# @deprecated
|
89
|
+
def safe_msg(*args)
|
90
|
+
Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Target#safe_msg", "Target#safe_send")
|
91
|
+
send(*args)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @deprecated
|
95
|
+
def safe_privmsg(*args)
|
96
|
+
Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Target#safe_privmsg", "Target#safe_send")
|
97
|
+
send(*args)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Like {#safe_msg} but for notices.
|
101
|
+
#
|
102
|
+
# @return (see #safe_msg)
|
103
|
+
# @param (see #safe_msg)
|
104
|
+
# @see #safe_notice
|
105
|
+
# @see #notice
|
106
|
+
def safe_notice(text)
|
107
|
+
safe_send(text, true)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Invoke an action (/me) in/to the target.
|
111
|
+
#
|
112
|
+
# @param [#to_s] text the message to send
|
113
|
+
# @return [void]
|
114
|
+
# @see #safe_action
|
115
|
+
def action(text)
|
116
|
+
line = text.to_s.each_line.first.chomp
|
117
|
+
@bot.irc.send("PRIVMSG #{@name} :\001ACTION #{line}\001")
|
118
|
+
end
|
119
|
+
|
120
|
+
# Like {#action}, but remove any non-printable characters from
|
121
|
+
# `text`. The purpose of this method is to send text from
|
122
|
+
# untrusted sources, like other users or feeds.
|
123
|
+
#
|
124
|
+
# Note: this will **break** any mIRC color codes embedded in the
|
125
|
+
# string. For more fine-grained control, use
|
126
|
+
# {Helpers#Sanitize} and
|
127
|
+
# {Formatting.unformat} directly.
|
128
|
+
#
|
129
|
+
# @param (see #action)
|
130
|
+
# @return (see #action)
|
131
|
+
# @see #action
|
132
|
+
def safe_action(text)
|
133
|
+
action(Cinch::Helpers.sanitize(text))
|
134
|
+
end
|
135
|
+
|
136
|
+
# Send a CTCP to the target.
|
137
|
+
#
|
138
|
+
# @param [#to_s] message the ctcp message
|
139
|
+
# @return [void]
|
140
|
+
def ctcp(message)
|
141
|
+
send "\001#{message}\001"
|
142
|
+
end
|
143
|
+
|
144
|
+
def concretize
|
145
|
+
if @bot.isupport["CHANTYPES"].include?(@name[0])
|
146
|
+
@bot.channel_list.find_ensured(@name)
|
147
|
+
else
|
148
|
+
@bot.user_list.find_ensured(@name)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Boolean]
|
153
|
+
def eql?(other)
|
154
|
+
self == other
|
155
|
+
end
|
156
|
+
|
157
|
+
# @param [Target, String] other
|
158
|
+
# @return [-1, 0, 1, nil]
|
159
|
+
def <=>(other)
|
160
|
+
casemapping = @bot.irc.isupport["CASEMAPPING"]
|
161
|
+
left = @name.irc_downcase(casemapping)
|
162
|
+
|
163
|
+
if other.is_a?(Target)
|
164
|
+
left <=> other.name.irc_downcase(casemapping)
|
165
|
+
elsif other.is_a?(String)
|
166
|
+
left <=> other.irc_downcase(casemapping)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def split_message(msg, prefix, split_start, split_end)
|
173
|
+
max_bytesize = 510 - prefix.bytesize
|
174
|
+
max_bytesize_without_end = max_bytesize - split_end.bytesize
|
175
|
+
|
176
|
+
if msg.bytesize <= max_bytesize
|
177
|
+
return [msg]
|
178
|
+
end
|
179
|
+
|
180
|
+
splitted = []
|
181
|
+
while msg.bytesize > max_bytesize_without_end
|
182
|
+
acc = 0
|
183
|
+
acc_rune_sizes = msg.each_char.map { |ch|
|
184
|
+
acc += ch.bytesize
|
185
|
+
}
|
186
|
+
|
187
|
+
max_rune = acc_rune_sizes.rindex { |bs| bs <= max_bytesize_without_end } || 0
|
188
|
+
r = [msg.rindex(/\s/, max_rune) || (max_rune + 1), 1].max
|
189
|
+
|
190
|
+
splitted << (msg[0...r] + split_end)
|
191
|
+
msg = split_start.tr(" ", "\cz") + msg[r..].lstrip
|
192
|
+
end
|
193
|
+
splitted << msg
|
194
|
+
|
195
|
+
# clean string from any substitute characters
|
196
|
+
splitted.map { |string| string.tr("\cz", " ") }
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/cinch/timer.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helpers"
|
4
|
+
|
5
|
+
module Cinch
|
6
|
+
# Timers are used for executing code in the future, either
|
7
|
+
# repeatedly or only once.
|
8
|
+
#
|
9
|
+
# In Cinch, two ways for creating timers are available:
|
10
|
+
#
|
11
|
+
# - The first way is by declaring them for a plugin, in which case
|
12
|
+
# they will start as soon as the bot connects to a server.
|
13
|
+
#
|
14
|
+
# - The second way is to dynamically create new timers in response
|
15
|
+
# to user input. A common example for this is an alarm clock
|
16
|
+
# plugin, which has to execute at a specific time.
|
17
|
+
#
|
18
|
+
# @see Helpers#Timer For dynamically creating timers
|
19
|
+
# @see Plugin::ClassMethods#timer For declaring timers in plugins
|
20
|
+
# @note It is possible to directly create instances of this class,
|
21
|
+
# but the referenced methods should suffice.
|
22
|
+
# @since 2.0.0
|
23
|
+
class Timer
|
24
|
+
include Helpers
|
25
|
+
|
26
|
+
# @return [Bot]
|
27
|
+
attr_reader :bot
|
28
|
+
|
29
|
+
# @return [Numeric] The interval (in seconds) of the timer
|
30
|
+
attr_accessor :interval
|
31
|
+
|
32
|
+
# @return [Boolean] If true, each invocation will be
|
33
|
+
# executed in a thread of its own.
|
34
|
+
attr_accessor :threaded
|
35
|
+
|
36
|
+
# @return [Proc]
|
37
|
+
attr_reader :block
|
38
|
+
|
39
|
+
# @return [Boolean]
|
40
|
+
attr_reader :started
|
41
|
+
|
42
|
+
# @return [Integer] The remaining number of shots before this timer
|
43
|
+
# will stop. This value will automatically reset after
|
44
|
+
# restarting the timer.
|
45
|
+
attr_accessor :shots
|
46
|
+
alias_method :threaded?, :threaded
|
47
|
+
alias_method :started?, :started
|
48
|
+
|
49
|
+
# @return [ThreadGroup]
|
50
|
+
# @api private
|
51
|
+
attr_reader :thread_group
|
52
|
+
|
53
|
+
# @param [Bot] bot The instance of {Bot} the timer is associated
|
54
|
+
# with
|
55
|
+
# @option options [Numeric] :interval The interval (in seconds) of
|
56
|
+
# the timer
|
57
|
+
# @option options [Integer] :shots (Float::INFINITY) How often should the
|
58
|
+
# timer fire?
|
59
|
+
# @option options [Boolean] :threaded (true) If true, each invocation will be
|
60
|
+
# executed in a thread of its own.
|
61
|
+
# @option options [Boolean] :start_automatically (true) If true,
|
62
|
+
# the timer will automatically start after the bot finished
|
63
|
+
# connecting.
|
64
|
+
# @option options [Boolean] :stop_automaticall (true) If true, the
|
65
|
+
# timer will automatically stop when the bot disconnects.
|
66
|
+
def initialize(bot, options, &block)
|
67
|
+
options = {threaded: true, shots: Float::INFINITY, start_automatically: true, stop_automatically: true}.merge(options)
|
68
|
+
|
69
|
+
@bot = bot
|
70
|
+
@interval = options[:interval].to_f
|
71
|
+
@threaded = options[:threaded]
|
72
|
+
@orig_shots = options[:shots]
|
73
|
+
# Setting @shots here so the attr_reader won't return nil
|
74
|
+
@shots = @orig_shots
|
75
|
+
@block = block
|
76
|
+
|
77
|
+
@started = false
|
78
|
+
@thread_group = ThreadGroup.new
|
79
|
+
|
80
|
+
if options[:start_automatically]
|
81
|
+
@bot.on :connect, //, self do |m, timer|
|
82
|
+
timer.start
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if options[:stop_automatically]
|
87
|
+
@bot.on :disconnect, //, self do |m, timer|
|
88
|
+
timer.stop
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [Boolean]
|
94
|
+
def stopped?
|
95
|
+
!@started
|
96
|
+
end
|
97
|
+
|
98
|
+
# Start the timer
|
99
|
+
#
|
100
|
+
# @return [void]
|
101
|
+
def start
|
102
|
+
return if @started
|
103
|
+
|
104
|
+
@bot.loggers.debug "[timer] Starting timer #{self}"
|
105
|
+
|
106
|
+
@shots = @orig_shots
|
107
|
+
|
108
|
+
@thread_group.add Thread.new {
|
109
|
+
while @shots > 0
|
110
|
+
sleep @interval
|
111
|
+
if threaded?
|
112
|
+
Thread.new do
|
113
|
+
rescue_exception do
|
114
|
+
@block.call
|
115
|
+
end
|
116
|
+
end
|
117
|
+
else
|
118
|
+
rescue_exception do
|
119
|
+
@block.call
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@shots -= 1
|
124
|
+
end
|
125
|
+
}
|
126
|
+
|
127
|
+
@started = true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Stop the timer
|
131
|
+
#
|
132
|
+
# @return [void]
|
133
|
+
def stop
|
134
|
+
return unless @started
|
135
|
+
|
136
|
+
@bot.loggers.debug "[timer] Stopping timer #{self}"
|
137
|
+
|
138
|
+
@thread_group.list.each { |thread| thread.kill }
|
139
|
+
@started = false
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [String]
|
143
|
+
def to_s
|
144
|
+
"<Cinch::Timer %s/%s shots, %ds interval, %sthreaded, %sstarted, block: %s>" % [@orig_shots - @shots, @orig_shots, @interval, @threaded ? "" : "not ", @started ? "" : "not ", @block]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|