vk_cozy 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84172a36d37fa7b748679799a2baedad22b63e88f03a59ede5742e0d44731221
4
- data.tar.gz: d5370aa9ca6b774890ebeb52006102d1d28e5aa6f2acbd38c67bbdcbd9bcab18
3
+ metadata.gz: 258b9457cabc9453353250a7f51db038240ef43bb1b93b68ba4128fdfb59031d
4
+ data.tar.gz: c35f4cbf0564408024d238ca505f28b076fdb57b40dddfcb338707b7847d82fc
5
5
  SHA512:
6
- metadata.gz: 9fcd0cf360e2f41524459621f8e1f388a2bb037629218ccd7274d5946e3a3cf805237302b092d4f3c4b1a084526c2057dbefb781c4485e2b876c1da6759f996e
7
- data.tar.gz: e89c98542748a160e64ab434873e3330f9a8124f981f2d56b2b003795d9d4994f10dacb1a1a404c65f9b75561b73f26e3aca4770a4857832288b4a6499c6aad2
6
+ metadata.gz: 8fe4888bcb9db9859ac768c5ec0ba3e5c70d2adc38a3513e535254daf942c09708e9cb461a1161099d0ed9b5470d76b9863593fccdd82856ed1361333fb3681b
7
+ data.tar.gz: 0dde51243767756401df2988a07befaa65e6df3ef0dfb9e11b4e3ab58a09ea15c16ddcc70d2ab6cbb6171ac196b02349a00a52a6c568d7235c6faff10bbb1e4d
@@ -0,0 +1,50 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require "resolv-replace"
5
+
6
+ class Api
7
+ attr_accessor :access_token, :version
8
+
9
+ SCHEME = 'https'
10
+ HOST = 'api.vk.com'
11
+ PATH = '/method/'
12
+ PORT = 443
13
+
14
+ def initialize(access_token, version=5.92)
15
+ @access_token = access_token
16
+ @version = version
17
+ end
18
+
19
+ def request_thr(method_vk, data)
20
+ thr = Thread.new {
21
+ request(method_vk, data)
22
+ }
23
+ thr.run
24
+ # thr.value
25
+ end
26
+
27
+ def request(method_vk, data)
28
+ data = data.to_hash
29
+ data = data.merge(v: version)
30
+ data = data.merge(access_token: access_token)
31
+ data.each do |argument, value|
32
+ data[argument] = value.join(',') if value.is_a?(Array)
33
+ end
34
+ http_response = Net::HTTP.post_form(url_for_method(method_vk), data).body
35
+ # return unless http_response.present?
36
+ json_response = JSON.parse(http_response)
37
+ if json_response['error']
38
+ raise json_response['error']['error_msg']
39
+ end
40
+ json_response
41
+ end
42
+
43
+ def url_for_method(method_vk)
44
+ URI.parse("#{SCHEME}://#{HOST}#{PATH}#{method_vk}")
45
+ end
46
+
47
+ def method_missing name, **kwargs
48
+ return request(name.to_s.sub('_', '.'), kwargs)['response']
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'types/events/bot_events'
2
+ require_relative 'dispatch/views/bot/event'
3
+ require_relative 'framework/labeler/bot'
4
+ require_relative 'polling/bot_polling'
5
+
6
+ module VkCozy
7
+ class Bot
8
+ attr_reader :api
9
+
10
+ CLASS_BY_EVENT_TYPE = {
11
+ BotEventType::MESSAGE_NEW => BotMessageEvent,
12
+ BotEventType::MESSAGE_REPLY => BotMessageEvent,
13
+ BotEventType::MESSAGE_EDIT => BotMessageEvent
14
+ }
15
+
16
+ DEFAULT_EVENT_CLASS = BotEvent
17
+
18
+ def initialize(access_token, version=5.92, api=nil)
19
+ @access_token = access_token
20
+ if api.nil?
21
+ @api = Api.new(access_token, version)
22
+ else
23
+ @api = api
24
+ end
25
+ @polling = VkCozy::BotPolling.new(@api)
26
+ @labeler = VkCozy::BotLabeler.new(@api)
27
+ end
28
+
29
+ def on
30
+ return @labeler
31
+ end
32
+
33
+ def run_polling
34
+ @polling.listen do |event|
35
+ for event_raw in event['updates']
36
+ begin
37
+ @labeler.filter(parse_event(event_raw))
38
+ rescue Exception => e
39
+ raise e
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def parse_event(event_raw)
48
+ event_class = CLASS_BY_EVENT_TYPE.fetch(
49
+ event_raw['type'],
50
+ DEFAULT_EVENT_CLASS
51
+ )
52
+ return event_class.new(@api, event_raw)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,67 @@
1
+ module VkCozy
2
+ class BotEvent
3
+ attr_reader :api, :raw, :t, :type, :obj, :object, :client_info, :message, :group_id
4
+ def initialize(api, event_raw)
5
+ @api = api
6
+ @raw = event_raw
7
+
8
+ @type = event_raw['type']
9
+ @t = @type
10
+
11
+ @object = event_raw['object'].to_dot
12
+ @obj = @object
13
+
14
+ @message = @obj['message']
15
+
16
+ @client_info = @obj['client_info']
17
+
18
+ @group_id = @raw['group_id']
19
+
20
+ end
21
+
22
+ def [] key
23
+ instance_variable_get("@#{key}")
24
+ end
25
+
26
+ def to_s
27
+ "BotEvent(#{@raw.to_s})"
28
+ end
29
+ end
30
+
31
+ class BotMessageEvent < BotEvent
32
+ attr_reader :from_user, :from_chat, :from_group, :chat_id
33
+ def initialize(api, event_raw)
34
+ super(api, event_raw)
35
+ @from_user = false
36
+ @from_chat = false
37
+ @from_group = false
38
+
39
+
40
+ peer_id = @raw['object']['peer_id']
41
+ if peer_id.nil?
42
+ peer_id = @raw['object']['message']['peer_id']
43
+ end
44
+
45
+ if peer_id < 0
46
+ @from_group = true
47
+ elsif peer_id < 2e9
48
+ @from_user = true
49
+ else
50
+ from_user = true
51
+ @chat_id = peer_id - 2e9
52
+ end
53
+ end
54
+
55
+ def answer(text)
56
+ return @api.messages_send(
57
+ peer_id: @message.peer_id,
58
+ message: text,
59
+ random_id: 0
60
+ )
61
+ end
62
+
63
+ def to_s
64
+ "BotMessageEvent(#{@raw.to_s})"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,80 @@
1
+ module VkCozy
2
+ class UserEvent
3
+ attr_accessor :api, :raw, :type,
4
+ :from_user, :from_chat, :from_group, :from_me, :to_me,
5
+ :attachments, :message_data,
6
+ :message_id, :timestamp, :text, :peer_id, :flags, :extra, :extra_values, :type_id
7
+
8
+ def initialize(api, raw_event)
9
+ @api = api
10
+ @raw = raw_event
11
+
12
+ @from_user = false
13
+ @from_chat = false
14
+ @from_group = false
15
+ @from_me = false
16
+ @to_me = false
17
+
18
+ begin
19
+ @type = UserEventType.parse(@raw[0])
20
+ list_to_attr(@raw[1, @raw.length], EVENT_ATTRS_MAPPING[@type])
21
+ rescue StandardError => e
22
+ @type = @raw[0]
23
+ end
24
+
25
+ if VkCozy::PARSE_PEER_ID_EVENTS.include?(@type)
26
+ parse_peer_id()
27
+ end
28
+
29
+ if VkCozy::PARSE_MESSAGE_FLAGS_EVENTS
30
+ parse_message()
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ instance_variables.each_with_object({}) do |k, h|
36
+ h[k] = instance_variable_get("#{k}")
37
+ end.to_json
38
+ end
39
+
40
+ def answer(text)
41
+ return @api.messages_send(
42
+ peer_id: @peer_id,
43
+ message: text,
44
+ random_id: 0
45
+ )
46
+ end
47
+
48
+ private
49
+
50
+ def list_to_attr(raw, attrs)
51
+ for i in (0..[raw.length, attrs.length].min)
52
+ instance_variable_set("@#{attrs[i]}", raw[i]) if respond_to? "#{attrs[i]}="
53
+ end
54
+ end
55
+
56
+ def parse_peer_id
57
+ if @peer_id < 0
58
+ @from_group = true
59
+ @group_id = peer_id
60
+
61
+ elsif @peer_id > 2e9
62
+ @from_chat = true
63
+ @chat_id = @peer_id-2e9
64
+
65
+ if @extra_values and @extra_values.include?('from')
66
+ @user_id = @extra_values['from'].to_i
67
+ end
68
+ else
69
+ @from_user = true
70
+ @user_id = @peer_id
71
+ end
72
+ end
73
+
74
+ def parse_message
75
+ if @type == UserEventType::MESSAGE_NEW
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,34 @@
1
+ module VkCozy
2
+ class BotLabeler
3
+ attr_reader :api
4
+
5
+ def initialize(api)
6
+ @api
7
+ @rules = []
8
+ end
9
+
10
+ def filter(event)
11
+ for i in @rules
12
+ f = i[:filter]
13
+ check = f.check_bot(event)
14
+ if check
15
+ if check.is_a?(Hash)
16
+ i[:func].call(event, **check)
17
+ else
18
+ i[:func].call(event)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def message_handler(filter, func)
25
+ if func.is_a?(Symbol)
26
+ func = method(func)
27
+ end
28
+ @rules << {
29
+ :func => func,
30
+ :filter => filter
31
+ }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ module VkCozy
2
+ class BaseFilter
3
+ def check_user(event) # Method check for user-bot
4
+ raise 'Method check_user not implemented'
5
+ end
6
+
7
+ def check_bot(event) # Method check for group-bot
8
+ raise 'Method check_bot not implemented'
9
+ end
10
+ end
11
+
12
+ class Text < BaseFilter
13
+ def initialize(regex, **kwargs)
14
+ @regex = regex
15
+ @raw = kwargs
16
+ end
17
+
18
+ def check_user(event)
19
+ if event.type == VkCozy::UserEventType::MESSAGE_NEW
20
+
21
+ if event.from_me
22
+ return false
23
+ end
24
+ if event.text == @regex
25
+ return true
26
+ else
27
+ return false
28
+ end
29
+ else
30
+ return false
31
+ end
32
+ end
33
+
34
+ def check_bot(event)
35
+ if event.type == VkCozy::BotEventType::MESSAGE_NEW
36
+ if event.message.text == @regex
37
+ return true
38
+ else
39
+ return false
40
+ end
41
+ else
42
+ return false
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ module VkCozy
2
+ class UserLabeler
3
+ attr_reader :api
4
+
5
+ def initialize(api)
6
+ @api = api
7
+ @rules = []
8
+ end
9
+
10
+ def filter(event_raw)
11
+ event = VkCozy::UserEvent.new(@api, event_raw)
12
+ for i in @rules
13
+ f = i[:filter]
14
+ check = f.check_user(event)
15
+ if check
16
+ if check.is_a?(Hash)
17
+ i[:func].call(event, **check)
18
+ else
19
+ i[:func].call(event)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def message_handler(filter, func)
26
+ if func.is_a?(Symbol)
27
+ func = method(func)
28
+ end
29
+ @rules << {
30
+ :func => func,
31
+ :filter => filter
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ module VkCozy
2
+ class BotPolling
3
+ def initialize(api, group_id=nil, wait=25, rps_delay=0)
4
+ @api = api
5
+ if group_id.nil?
6
+ @group_id = @api.request('groups.getById', {})['response'][0]['id']
7
+ puts @group_id
8
+ else
9
+ @group_id = group_id
10
+ end
11
+
12
+ @rps_delay = rps_delay
13
+ @stop = false
14
+ end
15
+
16
+ def get_server
17
+ return @api.request('groups.getLongPollServer', {:group_id => @group_id})['response']
18
+ end
19
+
20
+ def get_event(server)
21
+ uri = URI.parse('%s?act=a_check&key=%s&ts=%s&wait=%s&rps_delay=%s' % [server['server'], server['key'], server['ts'], @wait, @rps_delay])
22
+ http_response = Net::HTTP.get(uri)
23
+ return JSON.parse(http_response)
24
+ end
25
+
26
+ def listen
27
+ server = get_server
28
+ until @stop do
29
+ event = get_event(server)
30
+ if not event['ts']
31
+ server = get_server
32
+ next
33
+ end
34
+ server['ts'] = event['ts']
35
+ yield event
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ module VkCozy
2
+ class UserPolling
3
+ def initialize(api=nil, user_id=nil, mode=234, wait=25, rps_delay=0, error_handler=nil)
4
+ @api = api
5
+ if user_id.nil?
6
+ @user_id = @api.request('users.get', {})['response'][0]['id']
7
+ else
8
+ @user_id = user_id
9
+ end
10
+ @mode = mode
11
+ @wait = wait
12
+ @rps_delay = rps_delay
13
+ @stop = false
14
+ end
15
+
16
+ def get_event(server)
17
+ uri = URI.parse('https://%s?act=a_check&key=%s&ts=%s&wait=%s&mode=%s&rps_delay=%s&version=%s' % [server['server'], server['key'], server['ts'], @wait, @mode, @rps_delay, 3])
18
+ http_response = Net::HTTP.get(uri)
19
+ return JSON.parse(http_response)
20
+ end
21
+
22
+ def get_server
23
+ return @api.request('messages.getLongPollServer', {})['response']
24
+ end
25
+
26
+ def listen
27
+ server = get_server
28
+ until @stop do
29
+ event = get_event(server)
30
+ if not event['ts']
31
+ server = get_server
32
+ next
33
+ end
34
+ server['ts'] = event['ts']
35
+ yield event
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ module VkCozy
2
+ class BotEventType
3
+ include Ruby::Enum
4
+
5
+ define :MESSAGE_NEW, 'message_new'
6
+ define :MESSAGE_REPLY, 'message_reply'
7
+ define :MESSAGE_EDIT, 'message_edit'
8
+ define :MESSAGE_EVENT, 'message_event'
9
+
10
+ define :MESSAGE_TYPING_STATE, 'message_typing_state'
11
+
12
+ define :MESSAGE_ALLOW, 'message_allow'
13
+
14
+ define :MESSAGE_DENY, 'message_deny'
15
+
16
+ define :PHOTO_NEW, 'photo_new'
17
+
18
+ define :PHOTO_COMMENT_NEW, 'photo_comment_new'
19
+ define :PHOTO_COMMENT_EDIT, 'photo_comment_edit'
20
+ define :PHOTO_COMMENT_RESTORE, 'photo_comment_restore'
21
+
22
+ define :PHOTO_COMMENT_DELETE, 'photo_comment_delete'
23
+
24
+ define :AUDIO_NEW, 'audio_new'
25
+
26
+ define :VIDEO_NEW, 'video_new'
27
+
28
+ define :VIDEO_COMMENT_NEW, 'video_comment_new'
29
+ define :VIDEO_COMMENT_EDIT, 'video_comment_edit'
30
+ define :VIDEO_COMMENT_RESTORE, 'video_comment_restore'
31
+
32
+ define :VIDEO_COMMENT_DELETE, 'video_comment_delete'
33
+
34
+ define :WALL_POST_NEW, 'wall_post_new'
35
+ define :WALL_REPOST, 'wall_repost'
36
+
37
+ define :WALL_REPLY_NEW, 'wall_reply_new'
38
+ define :WALL_REPLY_EDIT, 'wall_reply_edit'
39
+ define :WALL_REPLY_RESTORE, 'wall_reply_restore'
40
+
41
+ define :WALL_REPLY_DELETE, 'wall_reply_delete'
42
+
43
+ define :BOARD_POST_NEW, 'board_post_new'
44
+ define :BOARD_POST_EDIT, 'board_post_edit'
45
+ define :BOARD_POST_RESTORE, 'board_post_restore'
46
+
47
+ define :BOARD_POST_DELETE, 'board_post_delete'
48
+
49
+ define :MARKET_COMMENT_NEW, 'market_comment_new'
50
+ define :MARKET_COMMENT_EDIT, 'market_comment_edit'
51
+ define :MARKET_COMMENT_RESTORE, 'market_comment_restore'
52
+
53
+ define :MARKET_COMMENT_DELETE, 'market_comment_delete'
54
+
55
+ define :GROUP_LEAVE, 'group_leave'
56
+
57
+ define :GROUP_JOIN, 'group_join'
58
+
59
+ define :USER_BLOCK, 'user_block'
60
+
61
+ define :USER_UNBLOCK, 'user_unblock'
62
+
63
+ define :POLL_VOTE_NEW, 'poll_vote_new'
64
+
65
+ define :GROUP_OFFICERS_EDIT, 'group_officers_edit'
66
+
67
+ define :GROUP_CHANGE_SETTINGS, 'group_change_settings'
68
+
69
+ define :GROUP_CHANGE_PHOTO, 'group_change_photo'
70
+
71
+ define :VKPAY_TRANSACTION, 'vkpay_transaction'
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ module VkCozy
2
+ class UserEventType < Inum::Base
3
+ define :UNDEFINED_EVENT, -1
4
+
5
+ define :REPLACE_MESSAGE_FLAGS, 1
6
+ define :INSTALL_MESSAGE_FLAGS, 2
7
+ define :RESET_MESSAGE_FLAGS, 3
8
+
9
+ define :MESSAGE_NEW, 4
10
+ define :MESSAGE_EDIT, 5
11
+ define :IN_READ, 6
12
+ define :OUT_READ, 7
13
+ define :FRIEND_ONLINE, 8
14
+ define :FRIEND_OFFLINE, 9
15
+ define :RESET_DIALOG_FLAGS, 10
16
+ define :REPLACE_DIALOG_FLAGS, 11
17
+ define :INSTALL_DIALOG_FLAGS, 12
18
+ define :MESSAGES_DELETE, 13
19
+ define :MESSAGES_RESTORE, 14
20
+
21
+ define :MESSAGE_CHANGE, 18
22
+ define :CLEAR_MESSAGE_CACHE, 19
23
+
24
+ define :CHANGE_MAJOR_ID, 20
25
+ define :CHANGE_MINOR_ID, 21
26
+
27
+ define :CHAT_EDIT, 51
28
+ define :CHAT_INFO_EDIT, 52
29
+ define :DIALOG_TYPING_STATE, 61
30
+
31
+ define :CHAT_TYPING_STATE, 62
32
+ define :USERS_TYPING_STATE, 63
33
+ define :CHAT_VOICE_MESSAGE_STATES, 64
34
+ define :PHOTO_UPLOAD_STATE, 65
35
+ define :VIDEO_UPLOAD_STATE, 66
36
+ define :FILE_UPLOAD_STAE, 67
37
+
38
+ define :CALL, 70
39
+ define :COUNTER, 80
40
+ define :USER_INVISIBLE_CHANGE, 81
41
+ define :NOTIFICATIONS_SETTINGS_CHANGED, 114
42
+ define :CHAT_CALL, 115
43
+ define :CALLBACK_BUTTON_REPLY, 119
44
+ end
45
+
46
+ MESSAGE_EXTRA_FIELDS = [
47
+ 'peer_id', 'timestamp', 'text', 'extra_values', 'attachments', 'random_id'
48
+ ]
49
+ MSGID = 'message_id'
50
+ EVENT_ATTRS_MAPPING = {
51
+ VkCozy::UserEventType::REPLACE_MESSAGE_FLAGS => [MSGID, 'flags'] + MESSAGE_EXTRA_FIELDS,
52
+ VkCozy::UserEventType::INSTALL_MESSAGE_FLAGS => [MSGID, 'mask'] + MESSAGE_EXTRA_FIELDS,
53
+ VkCozy::UserEventType::RESET_MESSAGE_FLAGS => [MSGID, 'mask'] + MESSAGE_EXTRA_FIELDS,
54
+ VkCozy::UserEventType::MESSAGE_NEW => [MSGID, 'flags'] + MESSAGE_EXTRA_FIELDS,
55
+ VkCozy::UserEventType::MESSAGE_EDIT => [MSGID, 'mask'] + MESSAGE_EXTRA_FIELDS,
56
+ VkCozy::UserEventType::IN_READ => ['peer_id', 'local_id'],
57
+ VkCozy::UserEventType::OUT_READ => ['peer_id', 'local_id'],
58
+ VkCozy::UserEventType::FRIEND_ONLINE => ['user_id', 'extra', 'timestamp'],
59
+ VkCozy::UserEventType::FRIEND_OFFLINE => ['user_id', 'flags', 'timestamp'],
60
+ VkCozy::UserEventType::DIALOG_TYPING_STATE => ['user_id', 'flags']
61
+ }
62
+ PARSE_PEER_ID_EVENTS = EVENT_ATTRS_MAPPING.map{|k, v| if v.include?('peer_id') then k end}.select{ |i| not i.nil? }
63
+ PARSE_MESSAGE_FLAGS_EVENTS = [
64
+ VkCozy::UserEventType::REPLACE_MESSAGE_FLAGS,
65
+ VkCozy::UserEventType::MESSAGE_NEW
66
+ ]
67
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'types/events/user_events'
2
+ require_relative 'dispatch/views/user/event'
3
+ require_relative 'framework/labeler/user'
4
+ require_relative 'polling/user_polling'
5
+
6
+ module VkCozy
7
+ class User
8
+ attr_reader :api
9
+
10
+ def initialize(access_token, version=5.92, api=nil)
11
+ @access_token = access_token
12
+ if api.nil?
13
+ @api = Api.new(access_token, version)
14
+ else
15
+ @api = api
16
+ end
17
+ @polling = VkCozy::UserPolling.new(@api)
18
+ @labeler = VkCozy::UserLabeler.new(@api)
19
+ end
20
+
21
+ def on
22
+ return @labeler
23
+ end
24
+
25
+ def run_polling
26
+ @polling.listen do |event|
27
+ for update in event['updates']
28
+ begin
29
+ @labeler.filter(update)
30
+ rescue Exception => e
31
+ raise e
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vk_cozy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danil Konenko
@@ -60,7 +60,19 @@ extra_rdoc_files: []
60
60
  files:
61
61
  - Gemfile
62
62
  - lib/vk_cozy.rb
63
- homepage: https://rubygems.org/gems/hola
63
+ - lib/vk_cozy/api/api.rb
64
+ - lib/vk_cozy/bot.rb
65
+ - lib/vk_cozy/dispatch/views/bot/event.rb
66
+ - lib/vk_cozy/dispatch/views/user/event.rb
67
+ - lib/vk_cozy/framework/labeler/bot.rb
68
+ - lib/vk_cozy/framework/labeler/filters/filters.rb
69
+ - lib/vk_cozy/framework/labeler/user.rb
70
+ - lib/vk_cozy/polling/bot_polling.rb
71
+ - lib/vk_cozy/polling/user_polling.rb
72
+ - lib/vk_cozy/types/events/bot_events.rb
73
+ - lib/vk_cozy/types/events/user_events.rb
74
+ - lib/vk_cozy/user.rb
75
+ homepage: https://github.com/VkCozy/vk_cozy
64
76
  licenses:
65
77
  - MIT
66
78
  metadata: {}