moostodon 0.1.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/lib/mastodon.rb +8 -0
- data/lib/mastodon/account.rb +62 -0
- data/lib/mastodon/app.rb +19 -0
- data/lib/mastodon/base.rb +52 -0
- data/lib/mastodon/card.rb +43 -0
- data/lib/mastodon/client.rb +32 -0
- data/lib/mastodon/collection.rb +30 -0
- data/lib/mastodon/emoji.rb +14 -0
- data/lib/mastodon/entities/app.rb +15 -0
- data/lib/mastodon/entities/hashtag.rb +15 -0
- data/lib/mastodon/entities/media.rb +31 -0
- data/lib/mastodon/entities/mention.rb +17 -0
- data/lib/mastodon/error.rb +37 -0
- data/lib/mastodon/headers.rb +20 -0
- data/lib/mastodon/instance.rb +25 -0
- data/lib/mastodon/list.rb +17 -0
- data/lib/mastodon/media.rb +37 -0
- data/lib/mastodon/notification.rb +32 -0
- data/lib/mastodon/relationship.rb +38 -0
- data/lib/mastodon/rest/accounts.rb +71 -0
- data/lib/mastodon/rest/api.rb +29 -0
- data/lib/mastodon/rest/apps.rb +28 -0
- data/lib/mastodon/rest/client.rb +13 -0
- data/lib/mastodon/rest/instances.rb +19 -0
- data/lib/mastodon/rest/media.rb +42 -0
- data/lib/mastodon/rest/notifications.rb +19 -0
- data/lib/mastodon/rest/relationships.rb +77 -0
- data/lib/mastodon/rest/request.rb +54 -0
- data/lib/mastodon/rest/search.rb +28 -0
- data/lib/mastodon/rest/statuses.rb +130 -0
- data/lib/mastodon/rest/suggestions.rb +19 -0
- data/lib/mastodon/rest/timelines.rb +47 -0
- data/lib/mastodon/rest/utils.rb +42 -0
- data/lib/mastodon/results.rb +19 -0
- data/lib/mastodon/status.rb +94 -0
- data/lib/mastodon/streaming/client.rb +103 -0
- data/lib/mastodon/streaming/connection.rb +47 -0
- data/lib/mastodon/streaming/deleted_status.rb +17 -0
- data/lib/mastodon/streaming/message_parser.rb +21 -0
- data/lib/mastodon/streaming/response.rb +45 -0
- data/lib/mastodon/version.rb +32 -0
- data/moostodon.gemspec +25 -0
- metadata +156 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/rest/utils'
|
4
|
+
require 'mastodon/status'
|
5
|
+
|
6
|
+
module Mastodon
|
7
|
+
module REST
|
8
|
+
module Search
|
9
|
+
include Mastodon::REST::Utils
|
10
|
+
|
11
|
+
# Search for content
|
12
|
+
# @param options [Hash]
|
13
|
+
# @option options :q [String] The search query
|
14
|
+
# @option options :resolve [Boolean] Whether to resolve non-local accounts
|
15
|
+
# @return [Mastodon::Results] If q is a URL, Mastodon will
|
16
|
+
# attempt to fetch the provided account or status. Otherwise, it
|
17
|
+
# will do a local account and hashtag search.
|
18
|
+
def search(query, options = {})
|
19
|
+
opts = {
|
20
|
+
q: query
|
21
|
+
}.merge(options)
|
22
|
+
|
23
|
+
perform_request_with_object(:get, '/api/v1/search', opts,
|
24
|
+
Mastodon::Results)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/rest/utils'
|
4
|
+
require 'mastodon/status'
|
5
|
+
|
6
|
+
module Mastodon
|
7
|
+
module REST
|
8
|
+
module Statuses
|
9
|
+
include Mastodon::REST::Utils
|
10
|
+
|
11
|
+
# @overload create_status(text, in_reply_to_id, media_ids, visibility)
|
12
|
+
# Create new status
|
13
|
+
# @param text [String]
|
14
|
+
# @param in_reply_to_id [Integer]
|
15
|
+
# @param media_ids [Array<Integer>]
|
16
|
+
# @param visibility [String]
|
17
|
+
# @return [Mastodon::Status]
|
18
|
+
# @overload create_status(text, args)
|
19
|
+
# Create new status
|
20
|
+
# @param text [String]
|
21
|
+
# @param options [Hash]
|
22
|
+
# @option options :in_reply_to_id [Integer]
|
23
|
+
# @option options :media_ids [Array<Integer>]
|
24
|
+
# @option options :visibility [String]
|
25
|
+
# @return <Mastodon::Status>
|
26
|
+
def create_status(text, *args)
|
27
|
+
params = normalize_status_params(*args)
|
28
|
+
params[:status] = text
|
29
|
+
params['media_ids[]'] ||= params.delete(:media_ids)
|
30
|
+
|
31
|
+
perform_request_with_object(:post, '/api/v1/statuses',
|
32
|
+
params, Mastodon::Status)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Retrieve status
|
36
|
+
# @param id [Integer]
|
37
|
+
# @return [Mastodon::Status]
|
38
|
+
def status(id)
|
39
|
+
perform_request_with_object(:get, "/api/v1/statuses/#{id}",
|
40
|
+
{}, Mastodon::Status)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Destroy status
|
44
|
+
# @param id [Integer]
|
45
|
+
# @return [Boolean]
|
46
|
+
def destroy_status(id)
|
47
|
+
!perform_request(:delete, "/api/v1/statuses/#{id}").nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
# Reblog a status
|
51
|
+
# @param id [Integer]
|
52
|
+
# @return [Mastodon::Status]
|
53
|
+
def reblog(id)
|
54
|
+
perform_request_with_object(:post, "/api/v1/statuses/#{id}/reblog",
|
55
|
+
{}, Mastodon::Status)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Undo a reblog of a status
|
59
|
+
# @param id [Integer]
|
60
|
+
# @return [Mastodon::Status]
|
61
|
+
def unreblog(id)
|
62
|
+
perform_request_with_object(:post, "/api/v1/statuses/#{id}/unreblog",
|
63
|
+
{}, Mastodon::Status)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Favourite a status
|
67
|
+
# @param id [Integer]
|
68
|
+
# @return [Mastodon::Status]
|
69
|
+
def favourite(id)
|
70
|
+
perform_request_with_object(:post, "/api/v1/statuses/#{id}/favourite",
|
71
|
+
{}, Mastodon::Status)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Undo a favourite of a status
|
75
|
+
# @param id [Integer]
|
76
|
+
# @return [Mastodon::Status]
|
77
|
+
def unfavourite(id)
|
78
|
+
perform_request_with_object(:post,
|
79
|
+
"/api/v1/statuses/#{id}/unfavourite",
|
80
|
+
{}, Mastodon::Status)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get a list of accounts that reblogged a toot
|
84
|
+
# @param id [Integer]
|
85
|
+
# @param options [Hash]
|
86
|
+
# @return [Mastodon::Collection<Mastodon::Account>]
|
87
|
+
def reblogged_by(id, options = {})
|
88
|
+
perform_request_with_collection(:get,
|
89
|
+
"/api/v1/statuses/#{id}/reblogged_by",
|
90
|
+
options, Mastodon::Account)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get a list of accounts that favourited a toot
|
94
|
+
# @param id [Integer]
|
95
|
+
# @param options [Hash]
|
96
|
+
# @return [Mastodon::Collection<Mastodon::Account>]
|
97
|
+
def favourited_by(id, options = {})
|
98
|
+
perform_request_with_collection(:get,
|
99
|
+
"/api/v1/statuses/#{id}/favourited_by",
|
100
|
+
options, Mastodon::Account)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get a list of statuses by a user
|
104
|
+
# @param account_id [Integer]
|
105
|
+
# @param options [Hash]
|
106
|
+
# @option options :max_id [Integer]
|
107
|
+
# @option options :since_id [Integer]
|
108
|
+
# @option options :limit [Integer]
|
109
|
+
# @return [Mastodon::Collection<Mastodon::Status>]
|
110
|
+
def statuses(account_id, options = {})
|
111
|
+
url = "/api/v1/accounts/#{account_id}/statuses"
|
112
|
+
perform_request_with_collection(:get, url, options, Mastodon::Status)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def normalize_status_params(*args)
|
118
|
+
if args.length == 1 && args.first.is_a?(Hash)
|
119
|
+
args.shift
|
120
|
+
else
|
121
|
+
{
|
122
|
+
in_reply_to_id: args.shift,
|
123
|
+
'media_ids[]' => args.shift,
|
124
|
+
visibility: args.shift
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/rest/utils'
|
4
|
+
require 'mastodon/account'
|
5
|
+
|
6
|
+
module Mastodon
|
7
|
+
module REST
|
8
|
+
module Suggestions
|
9
|
+
include Mastodon::REST::Utils
|
10
|
+
|
11
|
+
# Get "who to follow" suggestions for authenticated user
|
12
|
+
# @return [Mastodon::Collection<Mastodon::Account>]
|
13
|
+
def suggestions
|
14
|
+
perform_request_with_collection(:get, '/api/v1/accounts/suggestions',
|
15
|
+
{}, Mastodon::Account)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/rest/utils'
|
4
|
+
require 'mastodon/status'
|
5
|
+
|
6
|
+
module Mastodon
|
7
|
+
module REST
|
8
|
+
module Timelines
|
9
|
+
include Mastodon::REST::Utils
|
10
|
+
|
11
|
+
# Retrieve statuses from the home timeline
|
12
|
+
# @param options [Hash]
|
13
|
+
# @option options :max_id [Integer]
|
14
|
+
# @option options :since_id [Integer]
|
15
|
+
# @option options :limit [Integer]
|
16
|
+
# @return [Mastodon::Collection<Mastodon::Status>]
|
17
|
+
def home_timeline(options = {})
|
18
|
+
perform_request_with_collection(:get, '/api/v1/timelines/home',
|
19
|
+
options, Mastodon::Status)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieve statuses from the public timeline
|
23
|
+
# @param options [Hash]
|
24
|
+
# @option options :max_id [Integer]
|
25
|
+
# @option options :since_id [Integer]
|
26
|
+
# @option options :limit [Integer]
|
27
|
+
# @return [Mastodon::Collection<Mastodon::Status>]
|
28
|
+
def public_timeline(options = {})
|
29
|
+
perform_request_with_collection(:get, '/api/v1/timelines/public',
|
30
|
+
options, Mastodon::Status)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Retrieve statuses from a hashtag
|
34
|
+
# @param hashtag [String]
|
35
|
+
# @param options [Hash]
|
36
|
+
# @option options :max_id [Integer]
|
37
|
+
# @option options :since_id [Integer]
|
38
|
+
# @option options :limit [Integer]
|
39
|
+
# @return [Mastodon::Collection<Mastodon::Status>]
|
40
|
+
def hashtag_timeline(hashtag, options = {})
|
41
|
+
perform_request_with_collection(:get,
|
42
|
+
"/api/v1/timelines/tag/#{hashtag}",
|
43
|
+
options, Mastodon::Status)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/rest/request'
|
4
|
+
|
5
|
+
module Mastodon
|
6
|
+
module REST
|
7
|
+
module Utils
|
8
|
+
# @param request_method [Symbol]
|
9
|
+
# @param path [String]
|
10
|
+
# @param options [Hash]
|
11
|
+
def perform_request(request_method, path, options = {})
|
12
|
+
Mastodon::REST::Request.new(self, request_method, path, options).perform
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param request_method [Symbol]
|
16
|
+
# @param path [String]
|
17
|
+
# @param options [Hash]
|
18
|
+
# @param klass [Class]
|
19
|
+
def perform_request_with_object(request_method, path, options, klass)
|
20
|
+
response = perform_request(request_method, path, options)
|
21
|
+
klass.new(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param request_method [Symbol]
|
25
|
+
# @param path [String]
|
26
|
+
# @param options [Hash]
|
27
|
+
# @param klass [Class]
|
28
|
+
def perform_request_with_collection(request_method, path, options, klass)
|
29
|
+
response = perform_request(request_method, path, options)
|
30
|
+
Mastodon::Collection.new(response, klass)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Format an array of values into a query param
|
34
|
+
# @param key [Symbol]
|
35
|
+
# @param values [Enumerable]
|
36
|
+
# @return [Array]
|
37
|
+
def array_param(key, values)
|
38
|
+
values.map.with_index { |value, _i| ["#{key}[]", value] }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/account'
|
4
|
+
require 'mastodon/status'
|
5
|
+
|
6
|
+
module Mastodon
|
7
|
+
class Results < Mastodon::Base
|
8
|
+
# @!attribute [r] accounts
|
9
|
+
# @return [Mastodon::Collection<Mastodon::Account>]
|
10
|
+
# @!attribute [r] statuses
|
11
|
+
# @return [Mastodon::Collection<Mastodon::Status>]
|
12
|
+
# @!attribute [r] hashtags
|
13
|
+
# @return [Mastodon::Collection<String>]
|
14
|
+
|
15
|
+
collection_attr_reader :accounts, Mastodon::Account
|
16
|
+
collection_attr_reader :statuses, Mastodon::Status
|
17
|
+
collection_attr_reader :hashtags, String
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mastodon/account'
|
4
|
+
require 'mastodon/entities/media'
|
5
|
+
require 'mastodon/entities/mention'
|
6
|
+
require 'mastodon/entities/app'
|
7
|
+
require 'mastodon/entities/hashtag'
|
8
|
+
require 'mastodon/emoji'
|
9
|
+
|
10
|
+
module Mastodon
|
11
|
+
class Status < Mastodon::Base
|
12
|
+
# @!attribute [r] id
|
13
|
+
# @return [String]
|
14
|
+
# @!attribute [r] in_reply_to_id
|
15
|
+
# @return [String]
|
16
|
+
# @!attribute [r] in_reply_to_account_id
|
17
|
+
# @return [String]
|
18
|
+
# @!attribute [r] spoiler_text
|
19
|
+
# @return [String]
|
20
|
+
# @!attribute [r] content
|
21
|
+
# @return [String]
|
22
|
+
# @!attribute [r] url
|
23
|
+
# @return [String]
|
24
|
+
# @!attribute [r] uri
|
25
|
+
# @return [String]
|
26
|
+
# @!attribute [r] created_at
|
27
|
+
# @return [String]
|
28
|
+
# @!attribute [r] reblogs_count
|
29
|
+
# @return [Integer]
|
30
|
+
# @!attribute [r] favourites_count
|
31
|
+
# @return [Integer]
|
32
|
+
# @!attribute [r] visibility
|
33
|
+
# @return [String]
|
34
|
+
# @!attribute [r] language
|
35
|
+
# @return [String]
|
36
|
+
# @!attribute [r] account
|
37
|
+
# @return [Mastodon::Account]
|
38
|
+
# @!attribute [r] reblog
|
39
|
+
# @return [Mastodon::Status]
|
40
|
+
# @!attribute [r] application
|
41
|
+
# @return [Mastodon::Entities::App]
|
42
|
+
# @!attribute [r] favourited?
|
43
|
+
# @return [Boolean]
|
44
|
+
# @!attribute [r] reblogged?
|
45
|
+
# @return [Boolean]
|
46
|
+
# @!attribute [r] sensitive?
|
47
|
+
# @return [Boolean]
|
48
|
+
# @!attribute [r] muted?
|
49
|
+
# @return [Boolean]
|
50
|
+
# @!attribute [r] pinned?
|
51
|
+
# @return [Boolean]
|
52
|
+
# @!attribute [r] media_attachments
|
53
|
+
# @return [Mastodon::Collection<Mastodon::Entities::Media>]
|
54
|
+
# @!attribute [r] mentions
|
55
|
+
# @return [Mastodon::Collection<Mastodon::Entities::Mention>]
|
56
|
+
# @!attribute [r] tags
|
57
|
+
# @return [Mastodon::Collection<Mastodon::Entities::Hashtag>]
|
58
|
+
# @!attribute [r] emojis
|
59
|
+
# @return [Mastodon::Collection<Mastodon::Emoji>]
|
60
|
+
|
61
|
+
normal_attr_reader :id,
|
62
|
+
:content,
|
63
|
+
:in_reply_to_id,
|
64
|
+
:in_reply_to_account_id,
|
65
|
+
:url,
|
66
|
+
:uri,
|
67
|
+
:created_at,
|
68
|
+
:reblogs_count,
|
69
|
+
:favourites_count,
|
70
|
+
:visibility,
|
71
|
+
:spoiler_text,
|
72
|
+
:language
|
73
|
+
|
74
|
+
predicate_attr_reader :favourited,
|
75
|
+
:reblogged,
|
76
|
+
:sensitive,
|
77
|
+
:muted,
|
78
|
+
:pinned
|
79
|
+
|
80
|
+
object_attr_reader :account, Mastodon::Account
|
81
|
+
object_attr_reader :reblog, Mastodon::Status
|
82
|
+
object_attr_reader :application, Mastodon::Entities::App
|
83
|
+
|
84
|
+
collection_attr_reader :media_attachments, Mastodon::Entities::Media
|
85
|
+
collection_attr_reader :mentions, Mastodon::Entities::Mention
|
86
|
+
collection_attr_reader :emojis, Mastodon::Emoji
|
87
|
+
collection_attr_reader :tags, Mastodon::Entities::Hashtag
|
88
|
+
|
89
|
+
def initialize(attributes = {})
|
90
|
+
attributes.fetch('id')
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http/request'
|
4
|
+
require 'mastodon/client'
|
5
|
+
require 'mastodon/streaming/connection'
|
6
|
+
require 'mastodon/streaming/deleted_status'
|
7
|
+
require 'mastodon/streaming/message_parser'
|
8
|
+
require 'mastodon/streaming/response'
|
9
|
+
|
10
|
+
module Mastodon
|
11
|
+
module Streaming
|
12
|
+
# Streaming client class, to handle all streaming purposes.
|
13
|
+
class Client < Mastodon::Client
|
14
|
+
attr_writer :connection
|
15
|
+
|
16
|
+
# Initializes a new Client object
|
17
|
+
#
|
18
|
+
# @param options [Hash] A customizable set of options.
|
19
|
+
# @option options [String] :tcp_socket_class A class that Connection will
|
20
|
+
# use to create a new TCP socket.
|
21
|
+
# @option options [String] :ssl_socket_class A class that Connection will
|
22
|
+
# use to create a new SSL socket.
|
23
|
+
# @return [Mastodon::Streaming::Client]
|
24
|
+
def initialize(options = {})
|
25
|
+
super
|
26
|
+
options[:using_ssl] ||= base_url =~ /^https/
|
27
|
+
@connection = Streaming::Connection.new(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Streams messages for a single user
|
31
|
+
#
|
32
|
+
# @yield [Mastodon::Status, Mastodon::Notification,
|
33
|
+
# Mastodon::Streaming::DeletedStatus] A stream of Mastodon objects.
|
34
|
+
def user(options = {}, &block)
|
35
|
+
stream('user', options, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns statuses that contain the specified hashtag
|
39
|
+
#
|
40
|
+
# @yield [Mastodon::Status, Mastodon::Notification,
|
41
|
+
# Mastodon::Streaming::DeletedStatus] A stream of Mastodon objects.
|
42
|
+
def hashtag(tag, options = {}, &block)
|
43
|
+
options['tag'] = tag
|
44
|
+
stream('hashtag', options, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns all public statuses
|
48
|
+
#
|
49
|
+
# @yield [Mastodon::Status, Mastodon::Notification,
|
50
|
+
# Mastodon::Streaming::DeletedStatus] A stream of Mastodon objects.
|
51
|
+
def firehose(options = {}, &block)
|
52
|
+
stream('public', options, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Calls an arbitrary streaming endpoint and returns the results
|
57
|
+
# @yield [Mastodon::Status, Mastodon::Notification,
|
58
|
+
# Mastodon::Streaming::DeletedStatus] A stream of Mastodon objects.
|
59
|
+
def stream(path, options = {}, &block)
|
60
|
+
request(:get, "/api/v1/streaming/#{path}", options, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set a Proc to be run when connection established.
|
64
|
+
def before_request(&block)
|
65
|
+
if block_given?
|
66
|
+
@before_request = block
|
67
|
+
self
|
68
|
+
elsif instance_variable_defined?(:@before_request)
|
69
|
+
@before_request
|
70
|
+
else
|
71
|
+
proc {}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def request(method, path, params)
|
78
|
+
before_request.call
|
79
|
+
uri = Addressable::URI.parse(base_url + path)
|
80
|
+
|
81
|
+
headers = Mastodon::Headers.new(self).request_headers
|
82
|
+
|
83
|
+
request = HTTP::Request.new(verb: method,
|
84
|
+
uri: "#{uri}?#{to_url_params(params)}",
|
85
|
+
headers: headers)
|
86
|
+
response = Streaming::Response.new do |type, data|
|
87
|
+
# rubocop:disable AssignmentInCondition
|
88
|
+
if item = Streaming::MessageParser.parse(type, data)
|
89
|
+
yield(item)
|
90
|
+
end
|
91
|
+
# rubocop:enable AssignmentInCondition
|
92
|
+
end
|
93
|
+
@connection.stream(request, response)
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_url_params(params)
|
97
|
+
uri = Addressable::URI.new
|
98
|
+
uri.query_values = params
|
99
|
+
uri.query
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|