rubirai 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/CI.yml +64 -0
- data/.github/workflows/docs.yml +32 -0
- data/.github/workflows/pull_request.yml +34 -0
- data/.gitignore +145 -0
- data/.rubocop.yml +41 -0
- data/.yardopts +7 -0
- data/Gemfile +19 -0
- data/LICENSE +661 -0
- data/README.md +24 -0
- data/Rakefile +16 -0
- data/examples/helper.rb +3 -0
- data/examples/listener_example.rb +25 -0
- data/examples/simple_example.rb +24 -0
- data/lib/rubirai.rb +66 -0
- data/lib/rubirai/auth.rb +73 -0
- data/lib/rubirai/errors.rb +26 -0
- data/lib/rubirai/event_recv.rb +83 -0
- data/lib/rubirai/event_resp.rb +129 -0
- data/lib/rubirai/events/bot_events.rb +53 -0
- data/lib/rubirai/events/event.rb +115 -0
- data/lib/rubirai/events/message_events.rb +77 -0
- data/lib/rubirai/events/request_events.rb +35 -0
- data/lib/rubirai/events/rubirai_events.rb +17 -0
- data/lib/rubirai/listener.rb +44 -0
- data/lib/rubirai/listing.rb +37 -0
- data/lib/rubirai/management.rb +200 -0
- data/lib/rubirai/message.rb +84 -0
- data/lib/rubirai/messages/message.rb +306 -0
- data/lib/rubirai/messages/message_chain.rb +119 -0
- data/lib/rubirai/multipart.rb +44 -0
- data/lib/rubirai/objects/group.rb +23 -0
- data/lib/rubirai/objects/info.rb +71 -0
- data/lib/rubirai/objects/user.rb +30 -0
- data/lib/rubirai/plugin_info.rb +19 -0
- data/lib/rubirai/retcode.rb +18 -0
- data/lib/rubirai/session.rb +26 -0
- data/lib/rubirai/utils.rb +62 -0
- data/lib/rubirai/version.rb +9 -0
- data/misc/common.css +11 -0
- data/rubirai.gemspec +24 -0
- data/spec/auth_spec.rb +118 -0
- data/spec/error_spec.rb +30 -0
- data/spec/events/event_spec.rb +78 -0
- data/spec/message_spec.rb +12 -0
- data/spec/messages/message_chain_spec.rb +32 -0
- data/spec/messages/message_spec.rb +171 -0
- data/spec/plugin_info_spec.rb +28 -0
- data/spec/rubirai_bot_spec.rb +45 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/utils_spec.rb +70 -0
- metadata +121 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubirai
|
4
|
+
class Bot
|
5
|
+
# Uploads an image to QQ server
|
6
|
+
# @return [Hash] hash string keys are: `{ imageId, url, path }`
|
7
|
+
def upload_image(path_or_io, type = :friend)
|
8
|
+
self.class.ensure_type_in type, 'friend', 'group', 'temp'
|
9
|
+
call :post, '/uploadImage', form: {
|
10
|
+
sessionKey: @session,
|
11
|
+
type: type.to_s.downcase,
|
12
|
+
img: HTTP::FormData::File.new(path_or_io)
|
13
|
+
}, headers: { content_type: 'multipart/form-data' }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Uploads a voice file to QQ server
|
17
|
+
# Only group uploads available currently.
|
18
|
+
# @param path_or_io [String, Pathname, IO] path to voice file
|
19
|
+
# @return [Hash] hash string keys are: `{ voiceId, url, path }`
|
20
|
+
def upload_voice(path_or_io)
|
21
|
+
call :post, '/uploadVoice', form: {
|
22
|
+
sessionKey: @session,
|
23
|
+
type: 'group',
|
24
|
+
img: HTTP::FormData::File.new(path_or_io)
|
25
|
+
}, headers: { content_type: 'multipart/form-data' }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Uploads a file to a group (currently only groups supported)
|
29
|
+
# @param path_or_io [String, Pathname, IO] path to file
|
30
|
+
# @param target [Integer] group id
|
31
|
+
# @param group_path [String] path to file in group files
|
32
|
+
# @return [String] the string id of the mirai file
|
33
|
+
def upload_file_and_send(path_or_io, target, group_path)
|
34
|
+
res = call :post, '/uploadFileAndSend', form: {
|
35
|
+
sessionKey: @session,
|
36
|
+
type: 'Group',
|
37
|
+
target: target,
|
38
|
+
path: group_path,
|
39
|
+
file: HTTP::FormData::File.new(path_or_io)
|
40
|
+
}, headers: { content_type: 'multipart/form-data' }
|
41
|
+
res['id']
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubirai
|
4
|
+
class Group
|
5
|
+
attr_reader :bot, :id, :name, :permission
|
6
|
+
|
7
|
+
# @param hash [Hash{String => Object}]
|
8
|
+
# @param bot [Rubirai::Bot, nil]
|
9
|
+
def initialize(hash, bot = nil)
|
10
|
+
hash = hash.stringify_keys
|
11
|
+
@bot = bot
|
12
|
+
@id = hash['id']
|
13
|
+
@name = hash['name']
|
14
|
+
@permission = hash['permission']
|
15
|
+
end
|
16
|
+
|
17
|
+
class Permission
|
18
|
+
OWNER = 'OWNER'
|
19
|
+
ADMINISTRATOR = 'ADMINISTRATOR'
|
20
|
+
MEMBER = 'MEMBER'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubirai/utils'
|
4
|
+
|
5
|
+
module Rubirai
|
6
|
+
# @abstract
|
7
|
+
class Info
|
8
|
+
# @!method initialize(hash, bot = nil)
|
9
|
+
# @param hash [Hash{String => Object}]
|
10
|
+
# @param bot [Rubirai::Bot, nil]
|
11
|
+
# @!method to_h
|
12
|
+
# @return [Hash{String => Object}]
|
13
|
+
def self.set_fields(*fields, **default_values)
|
14
|
+
attr_reader(*fields)
|
15
|
+
|
16
|
+
class_eval do
|
17
|
+
define_method(:initialize) do |hash, bot = nil|
|
18
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
19
|
+
super hash, bot
|
20
|
+
fields.each do |field|
|
21
|
+
value = hash[field.to_s.snake_to_camel(lower: true)] || default_values[field]
|
22
|
+
instance_variable_set("@#{field}", value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method(:to_h) do
|
27
|
+
fields.to_h do |field|
|
28
|
+
[field.to_s.snake_to_camel(lower: true), instance_variable_get(field)]
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.set_modifiable_fields(*fields)
|
35
|
+
set_fields(*fields)
|
36
|
+
attr_writer(*fields)
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :raw, :bot
|
40
|
+
|
41
|
+
def initialize(hash, bot = nil)
|
42
|
+
@raw = hash
|
43
|
+
@bot = bot
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class GroupConfig < Info
|
48
|
+
set_modifiable_fields :name, :announcement, :confess_talk, :allow_member_invite, :auto_approve, :anonymous_chat
|
49
|
+
end
|
50
|
+
|
51
|
+
class MemberInfo < Info
|
52
|
+
set_fields :name, :nick, :special_title
|
53
|
+
attr_writer :name, :special_title
|
54
|
+
end
|
55
|
+
|
56
|
+
class GroupFileSimple < Info
|
57
|
+
set_fields :name, :id, :path, :is_file, is_file: true
|
58
|
+
end
|
59
|
+
|
60
|
+
class GroupFile < GroupFileSimple
|
61
|
+
set_fields :length,
|
62
|
+
:download_times,
|
63
|
+
:uploader_id,
|
64
|
+
:upload_time,
|
65
|
+
:last_modify_time,
|
66
|
+
:download_url,
|
67
|
+
:sha1,
|
68
|
+
:md5,
|
69
|
+
call_super: true
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'group'
|
4
|
+
require 'rubirai/errors'
|
5
|
+
|
6
|
+
module Rubirai
|
7
|
+
class User
|
8
|
+
attr_reader :bot, :id, :name, :remark
|
9
|
+
|
10
|
+
def initialize(hash, bot = nil)
|
11
|
+
hash = hash.stringify_keys
|
12
|
+
@bot = bot
|
13
|
+
@id = hash['id']
|
14
|
+
@name = hash['name'] || hash['nickname']
|
15
|
+
@remark = hash['remark']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class GroupUser < User
|
20
|
+
attr_reader :member_name, :permission, :group
|
21
|
+
|
22
|
+
def initialize(hash, bot = nil)
|
23
|
+
raise(RubiraiError, 'not a group user') unless hash.key? 'group'
|
24
|
+
super(hash, bot)
|
25
|
+
@member_name = hash['memberName']
|
26
|
+
@permission = hash['permission']
|
27
|
+
@group = Group.new(hash['group'], bot)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubirai
|
4
|
+
class Bot
|
5
|
+
# Get Mirai API plugin information such as
|
6
|
+
#
|
7
|
+
# ```ruby
|
8
|
+
# {
|
9
|
+
# 'version' => '1.0.0'
|
10
|
+
# }
|
11
|
+
# ```
|
12
|
+
#
|
13
|
+
# @return [Hash{String => Object}] plugin data
|
14
|
+
def about
|
15
|
+
v = call :get, '/about'
|
16
|
+
v['data']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubirai
|
4
|
+
# The return codes and their explanations
|
5
|
+
RETURN_CODE = {
|
6
|
+
0 => 'OK',
|
7
|
+
1 => 'Wrong auth key',
|
8
|
+
2 => 'No such bot',
|
9
|
+
3 => 'Session disappeared',
|
10
|
+
4 => 'Session not verified',
|
11
|
+
5 => 'No such receiver',
|
12
|
+
6 => 'No such file',
|
13
|
+
10 => 'No permission',
|
14
|
+
20 => 'Bot muted',
|
15
|
+
30 => 'Message too long',
|
16
|
+
400 => 'Bad request'
|
17
|
+
}.freeze
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rubirai
|
4
|
+
class Bot
|
5
|
+
# Get the config related to this session
|
6
|
+
# @return [Hash{String => Object}] the config
|
7
|
+
def get_session_cfg
|
8
|
+
call :get, '/config', params: {
|
9
|
+
sessionKey: @session
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Set the config related to this session
|
14
|
+
# @param cache_size [Integer, nil] the cache size
|
15
|
+
# @param enable_websocket [Boolean, nil] if to enable websocket
|
16
|
+
# @return [void]
|
17
|
+
def set_session_cfg(cache_size: nil, enable_websocket: nil)
|
18
|
+
call :post, '/config', json: {
|
19
|
+
sessionKey: @session,
|
20
|
+
cacheSize: cache_size,
|
21
|
+
enableWebsocket: enable_websocket
|
22
|
+
}.compact
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @private
|
4
|
+
class Hash
|
5
|
+
def stringify_keys!
|
6
|
+
transform_keys!(&:to_s)
|
7
|
+
end
|
8
|
+
|
9
|
+
def symbolize_keys!
|
10
|
+
transform_keys!(&:to_sym)
|
11
|
+
end
|
12
|
+
|
13
|
+
def stringify_keys
|
14
|
+
transform_keys(&:to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
def symbolize_keys
|
18
|
+
transform_keys(&:to_sym)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
class String
|
24
|
+
def snake_to_camel(lower: false)
|
25
|
+
s = split('_').collect(&:capitalize).join
|
26
|
+
return s unless lower
|
27
|
+
# noinspection RubyNilAnalysis
|
28
|
+
s[0] = s[0].downcase
|
29
|
+
s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @private
|
34
|
+
class Object
|
35
|
+
def must_be!(types, exc_type = nil, *args)
|
36
|
+
ok = case types
|
37
|
+
when Array
|
38
|
+
types.any? { |type| is_a? type }
|
39
|
+
else
|
40
|
+
is_a? types
|
41
|
+
end
|
42
|
+
self.class.raise_or_default exc_type, "assert failed: `#{self}' must be of type #{types}", *args unless ok
|
43
|
+
end
|
44
|
+
|
45
|
+
def must_be_one_of!(things, exc_type = nil, *args)
|
46
|
+
ok = case things
|
47
|
+
when Array
|
48
|
+
things.include? self
|
49
|
+
else
|
50
|
+
self == things
|
51
|
+
end
|
52
|
+
self.class.raise_or_default exc_type, "assert failed: `#{self}' must be one of: #{things}", *args unless ok
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.raise_or_default(exc_type, msg, *args)
|
56
|
+
if exc_type.nil?
|
57
|
+
raise(msg)
|
58
|
+
else
|
59
|
+
raise(exc_type, *args)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/misc/common.css
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Noto+Sans:ital,wght@0,400;0,700;1,400&family=Roboto+Mono:ital,wght@0,400;0,500;1,400&display=swap');
|
2
|
+
|
3
|
+
body, .summary_desc, .note.title, p.signature .extras, h3.signature .extras,
|
4
|
+
h1, h2, h3, h4, h5, h6, h3.signature .aliases {
|
5
|
+
font-family: 'SF Pro', 'Noto Sans', 'Noto Sans SC', Arial, Helvetica, -apple-system, sans-serif !important;
|
6
|
+
}
|
7
|
+
|
8
|
+
pre, code, tt, dt code, table tr td dt code, ul.summary, h3.signature .aliases .names,
|
9
|
+
p.signature, h3.signature, .tags ul .name, p.inherited a {
|
10
|
+
font-family: 'Menlo', 'Roboto Mono', monospace !important;
|
11
|
+
}
|
data/rubirai.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
require 'rubirai/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'rubirai'
|
8
|
+
s.version = Rubirai::VERSION
|
9
|
+
s.summary = 'A Mirai QQ bot http interface lib for Ruby.'
|
10
|
+
s.description = 'A Mirai QQ bot http interface lib for Ruby.'
|
11
|
+
s.authors = ['Rebuild']
|
12
|
+
s.email = 'admin@rebuild.moe'
|
13
|
+
s.licenses = ['AGPLv3']
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.require_paths = ['lib']
|
17
|
+
|
18
|
+
s.required_ruby_version = '>= 2.6'
|
19
|
+
|
20
|
+
s.add_dependency 'concurrent-ruby', '~> 1.1.8'
|
21
|
+
s.add_dependency 'http', '~> 5.0'
|
22
|
+
s.homepage = 'https://github.com/Shimogawa/rubirai'
|
23
|
+
s.license = 'AGPLv3'
|
24
|
+
end
|
data/spec/auth_spec.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'auth api' do
|
6
|
+
before :all do
|
7
|
+
@mirai_bot = new_bot
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
# Do nothing
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should be able to authenticate' do
|
15
|
+
stub_request(:post, @mirai_bot.gen_uri('/auth'))
|
16
|
+
.with(body: {
|
17
|
+
"authKey": @auth_key
|
18
|
+
})
|
19
|
+
.to_return(status: 200, body: %({
|
20
|
+
"code": 0,
|
21
|
+
"session": "#{@session_key}"
|
22
|
+
}))
|
23
|
+
res = @mirai_bot.auth @auth_key
|
24
|
+
expect(res).to be_a_kind_of(String)
|
25
|
+
expect(res).to eq(@session_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should raise error if authenticate fails' do
|
29
|
+
mirai_bot = new_bot
|
30
|
+
stub_request(:post, @mirai_bot.gen_uri('/auth'))
|
31
|
+
.with(body: {
|
32
|
+
"authKey": @auth_key
|
33
|
+
})
|
34
|
+
.to_return(status: 200, body: %({
|
35
|
+
"code": 1,
|
36
|
+
"session": ""
|
37
|
+
}))
|
38
|
+
|
39
|
+
expect { mirai_bot.auth @auth_key }.to raise_error(
|
40
|
+
Rubirai::MiraiError,
|
41
|
+
'Mirai error: 1 - Wrong auth key'
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should be able to verify' do
|
46
|
+
stub_request(:post, @mirai_bot.gen_uri('/verify'))
|
47
|
+
.with(body: {
|
48
|
+
"sessionKey": @session_key,
|
49
|
+
"qq": @qq
|
50
|
+
})
|
51
|
+
.to_return(status: 200, body: %({
|
52
|
+
"code": 0,
|
53
|
+
"session": "success"
|
54
|
+
}))
|
55
|
+
|
56
|
+
expect { @mirai_bot.verify @qq }.not_to raise_error
|
57
|
+
expect(@mirai_bot.verify(@qq)).to be_nil
|
58
|
+
|
59
|
+
expect { @mirai_bot.verify '1ab39cde' }.to raise_error(Rubirai::RubiraiError, 'Wrong format for qq')
|
60
|
+
expect { new_bot.verify @qq }.to raise_error(Rubirai::RubiraiError, 'No session provided')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should be able to release' do
|
64
|
+
stub_request(:post, @mirai_bot.gen_uri('/release'))
|
65
|
+
.with(body: {
|
66
|
+
"sessionKey": @session_key,
|
67
|
+
"qq": @qq
|
68
|
+
})
|
69
|
+
.to_return(status: 200, body: %({
|
70
|
+
"code": 0,
|
71
|
+
"msg": "success"
|
72
|
+
}))
|
73
|
+
|
74
|
+
expect do
|
75
|
+
expect(@mirai_bot.release(@qq)).to be_nil
|
76
|
+
end.not_to raise_error
|
77
|
+
expect(@mirai_bot.session).to be_nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should be able to login and logout' do
|
81
|
+
mirai_bot = Rubirai::Bot.new 'test'
|
82
|
+
stub_request(:post, @mirai_bot.gen_uri('/auth'))
|
83
|
+
.with(body: {
|
84
|
+
"authKey": @auth_key
|
85
|
+
})
|
86
|
+
.to_return(status: 200, body: %({
|
87
|
+
"code": 0,
|
88
|
+
"session": "#{@session_key}"
|
89
|
+
}))
|
90
|
+
stub_request(:post, @mirai_bot.gen_uri('/verify'))
|
91
|
+
.with(body: {
|
92
|
+
"sessionKey": @session_key,
|
93
|
+
"qq": @qq
|
94
|
+
})
|
95
|
+
.to_return(status: 200, body: %({
|
96
|
+
"code": 0,
|
97
|
+
"session": "success"
|
98
|
+
}))
|
99
|
+
stub_request(:post, @mirai_bot.gen_uri('/release'))
|
100
|
+
.with(body: {
|
101
|
+
"sessionKey": @session_key,
|
102
|
+
"qq": @qq
|
103
|
+
})
|
104
|
+
.to_return(status: 200, body: %({
|
105
|
+
"code": 0,
|
106
|
+
"msg": "success"
|
107
|
+
}))
|
108
|
+
expect do
|
109
|
+
expect(mirai_bot.login(@qq, @auth_key)).to be_nil
|
110
|
+
end.not_to raise_error
|
111
|
+
expect(mirai_bot.instance_variable_get('@session')).to eq(@session_key)
|
112
|
+
|
113
|
+
expect do
|
114
|
+
expect(mirai_bot.logout).to be_nil
|
115
|
+
end.not_to raise_error
|
116
|
+
expect(mirai_bot.instance_variable_get('@session')).to be_nil
|
117
|
+
end
|
118
|
+
end
|