rubirai 0.0.2

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/CI.yml +64 -0
  4. data/.github/workflows/docs.yml +32 -0
  5. data/.github/workflows/pull_request.yml +34 -0
  6. data/.gitignore +145 -0
  7. data/.rubocop.yml +41 -0
  8. data/.yardopts +7 -0
  9. data/Gemfile +19 -0
  10. data/LICENSE +661 -0
  11. data/README.md +24 -0
  12. data/Rakefile +16 -0
  13. data/examples/helper.rb +3 -0
  14. data/examples/listener_example.rb +25 -0
  15. data/examples/simple_example.rb +24 -0
  16. data/lib/rubirai.rb +66 -0
  17. data/lib/rubirai/auth.rb +73 -0
  18. data/lib/rubirai/errors.rb +26 -0
  19. data/lib/rubirai/event_recv.rb +83 -0
  20. data/lib/rubirai/event_resp.rb +129 -0
  21. data/lib/rubirai/events/bot_events.rb +53 -0
  22. data/lib/rubirai/events/event.rb +115 -0
  23. data/lib/rubirai/events/message_events.rb +77 -0
  24. data/lib/rubirai/events/request_events.rb +35 -0
  25. data/lib/rubirai/events/rubirai_events.rb +17 -0
  26. data/lib/rubirai/listener.rb +44 -0
  27. data/lib/rubirai/listing.rb +37 -0
  28. data/lib/rubirai/management.rb +200 -0
  29. data/lib/rubirai/message.rb +84 -0
  30. data/lib/rubirai/messages/message.rb +306 -0
  31. data/lib/rubirai/messages/message_chain.rb +119 -0
  32. data/lib/rubirai/multipart.rb +44 -0
  33. data/lib/rubirai/objects/group.rb +23 -0
  34. data/lib/rubirai/objects/info.rb +71 -0
  35. data/lib/rubirai/objects/user.rb +30 -0
  36. data/lib/rubirai/plugin_info.rb +19 -0
  37. data/lib/rubirai/retcode.rb +18 -0
  38. data/lib/rubirai/session.rb +26 -0
  39. data/lib/rubirai/utils.rb +62 -0
  40. data/lib/rubirai/version.rb +9 -0
  41. data/misc/common.css +11 -0
  42. data/rubirai.gemspec +24 -0
  43. data/spec/auth_spec.rb +118 -0
  44. data/spec/error_spec.rb +30 -0
  45. data/spec/events/event_spec.rb +78 -0
  46. data/spec/message_spec.rb +12 -0
  47. data/spec/messages/message_chain_spec.rb +32 -0
  48. data/spec/messages/message_spec.rb +171 -0
  49. data/spec/plugin_info_spec.rb +28 -0
  50. data/spec/rubirai_bot_spec.rb +45 -0
  51. data/spec/spec_helper.rb +31 -0
  52. data/spec/utils_spec.rb +70 -0
  53. 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubirai
4
+ # Rubirai version
5
+ VERSION = '0.0.2'
6
+
7
+ # mirai-api-http version
8
+ MIRAI_API_VERSION = '1.10.0'
9
+ 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