hivemind-ruby 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.
@@ -0,0 +1,90 @@
1
+ require 'active_record'
2
+ require 'safe_attributes'
3
+ require 'composite_primary_keys'
4
+
5
+ begin
6
+ require 'marginalia'
7
+ rescue LoadError
8
+ # skip
9
+ end
10
+
11
+ module Hive
12
+
13
+ # Abstract calss for all {Hive} active record classes.
14
+ class Base < ActiveRecord::Base
15
+ include SafeAttributes
16
+
17
+ self.abstract_class = true
18
+
19
+ database_url = ENV.fetch('DATABASE_URL', 'postgresql://user:pass@localhost:5432/hive')
20
+ database_uri = URI.parse(database_url)
21
+ database = database_uri.path.split('/').last
22
+
23
+ establish_connection({
24
+ adapter: database_uri.scheme,
25
+ host: ENV.fetch('HIVE_HOST', database_uri.host),
26
+ port: ENV.fetch('HIVE_PORT', database_uri.port),
27
+ username: ENV.fetch('HIVE_USERNAME', database_uri.user),
28
+ password: ENV.fetch('HIVE_PASSWORD', database_uri.password),
29
+ database: ENV.fetch('HIVE_DATABASE', database),
30
+ timeout: 60
31
+ })
32
+
33
+ scope :invertable, lambda { |expression, args, options = {invert: false}|
34
+ args = [args].flatten
35
+
36
+ if !!options[:invert]
37
+ where.not(expression, *args)
38
+ else
39
+ where(expression, *args)
40
+ end
41
+ }
42
+
43
+ # Please note, these are not comprehensive safeguards for marking records
44
+ # read-only. It's better to completely revoke write access from the user
45
+ # role at the lowest DBMS permission level.
46
+
47
+ after_initialize :readonly!
48
+
49
+ # Disabled
50
+ def self.delete(_=nil); raise ActiveRecord::ReadOnlyRecord; end
51
+
52
+ # Disabled
53
+ def self.delete_all(_=nil); raise ActiveRecord::ReadOnlyRecord; end
54
+
55
+ # Disabled
56
+ def self.update_all(_=nil); raise ActiveRecord::ReadOnlyRecord; end
57
+
58
+ # Disabled
59
+ def delete(_=nil); raise ActiveRecord::ReadOnlyRecord; end
60
+
61
+ # Disabled
62
+ def update(*_); raise ActiveRecord::ReadOnlyRecord; end
63
+
64
+ # Uses ducktyping to query on account so that you can pass any type and
65
+ # still get a valid match.
66
+ #
67
+ # So you can do:
68
+ #
69
+ # Hive::Post.where(author: 'alice')
70
+ # Hive::Post.where(author: %w(alice bob))
71
+ # Hive::Post.where(author: Hive::Account.find_by_name('alice'))
72
+ # Hive::Post.where(author: Hive::Account.where('name LIKE ?', '%z%'))
73
+ #
74
+ # @param account {String|<String>|Account|ActiveRecord::Relation} the
75
+ # account(s) to match on
76
+ def self.transform_account_selector(account)
77
+ case account
78
+ when String then account
79
+ when Array then account
80
+ when Account then account.name
81
+ else
82
+ unless account.is_a? ActiveRecord::Relation
83
+ raise "Don't know what to do with: #{account.class}"
84
+ end
85
+
86
+ account.select(:name)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,88 @@
1
+ module Hive
2
+
3
+ # Tracks blocks.
4
+ #
5
+ # To find blocks that have promoted posts:
6
+ #
7
+ # blocks = Hive::Block.with_promoted_posts
8
+ #
9
+ # With this result, we can get a list of post promoters in that block:
10
+ #
11
+ # accounts = blocks.first.post_promoters
12
+ #
13
+ # To find blocks that have normal intervals:
14
+ #
15
+ # blocks = Hive::Block.block_interval(is: 3.seconds)
16
+ #
17
+ # To find blocks that have longer than normal intervals:
18
+ #
19
+ # blocks = Hive::Block.block_interval(min: 4.seconds)
20
+ #
21
+ # To find blocks that have shorter than normal intervals:
22
+ #
23
+ # blocks = Hive::Block.block_interval(max: 2.seconds)
24
+ #
25
+ # To find blocks that have intervals in a certain range:
26
+ #
27
+ # blocks = Hive::Block.block_interval(min: 15.seconds, max: 30.seconds)
28
+ #
29
+ # Full report of slow blocks on the entire chain, grouped by date, sorted by
30
+ # the total slow blocks for that day:
31
+ #
32
+ # puts JSON.pretty_generate Hive::Block.block_interval(min: 4.seconds).
33
+ # group('CAST(hive_blocks.created_at AS DATE)').order("count_all").count
34
+ #
35
+ # Same report as above, grouped by month, sorted by the total slow blocks for
36
+ # that month:
37
+ #
38
+ # puts JSON.pretty_generate Hive::Block.block_interval(min: 4.seconds).
39
+ # group("TO_CHAR(hive_blocks.created_at, 'YYYY-MM')").order("count_all").count
40
+ #
41
+ # Same report as above, sorted by month:
42
+ #
43
+ # puts JSON.pretty_generate Hive::Block.block_interval(min: 4.seconds).
44
+ # group("TO_CHAR(hive_blocks.created_at, 'YYYY-MM')").
45
+ # order("to_char_hive_blocks_created_at_yyyy_mm").count
46
+ #
47
+ class Block < Base
48
+ self.table_name = :hive_blocks
49
+ self.primary_key = :num
50
+
51
+ belongs_to :previous, primary_key: :hash, foreign_key: :prev, class_name: 'Block'
52
+ belongs_to :next, primary_key: :prev, foreign_key: :hash, class_name: 'Block'
53
+ has_many :payments, foreign_key: :block_num
54
+ has_many :promoted_posts, through: :payments, source: :post
55
+ has_many :post_promoters, through: :payments, source: :from
56
+
57
+ scope :with_promoted_posts, -> { where num: Payment.select(:block_num) }
58
+ scope :decorate_block_interval, lambda { |units = :seconds|
59
+ columns = [arel_table[Arel.star]]
60
+ columns << "#{block_interval_column(units)} AS block_interval"
61
+
62
+ joins(:previous).select(columns)
63
+ }
64
+ scope :block_interval, lambda { |options = {is: 3.seconds}|
65
+ is = options[:is]
66
+ min = options[:min] || is
67
+ max = options[:max] || is
68
+ r = joins(:previous)
69
+
70
+ if !!min && !!max
71
+ if min == max
72
+ r = r.where("#{block_interval_column} = ?", min)
73
+ else
74
+ r = r.where("#{block_interval_column} BETWEEN ? AND ?", min, max)
75
+ end
76
+ else
77
+ r = r.where("#{block_interval_column} <= ?", max) if !!max
78
+ r = r.where("#{block_interval_column} >= ?", min) if !!min
79
+ end
80
+
81
+ r
82
+ }
83
+ private
84
+ def self.block_interval_column(units = :seconds)
85
+ "EXTRACT(#{units} FROM hive_blocks.created_at - previous_hive_blocks.created_at)"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,13 @@
1
+ module Hive
2
+
3
+ # Tracks communities.
4
+ class Community < Base
5
+ self.table_name = :hive_communities
6
+ self.primary_key = :name
7
+
8
+ has_many :posts, primary_key: :name, foreign_key: :community, inverse_of: :community_record
9
+ has_many :post_tags, through: :posts
10
+ has_many :members, primary_key: :name, foreign_key: :community
11
+ has_many :accounts, through: :members
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Hive
2
+
3
+ # Tracks feed cache.
4
+ class FeedCache < Base
5
+ self.table_name = :hive_feed_cache
6
+ self.primary_keys = %i(post_id account_id)
7
+
8
+ belongs_to :post
9
+ belongs_to :account
10
+
11
+ scope :posts, lambda { |post| where(post: post) }
12
+ scope :accounts, lambda { |account| where(account: account) }
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Hive
2
+
3
+ # Tracks post flags.
4
+ class Flag < Base
5
+ self.table_name = :hive_flags
6
+ self.primary_keys = %i(account post_id)
7
+
8
+ belongs_to :post
9
+ belongs_to :account_record, primary_key: :name, foreign_key: :account, class_name: 'Account'
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Hive
2
+
3
+ # Tracks follows, mutes, and follow/mute resets.
4
+ class Follow < Base
5
+ self.table_name = :hive_follows
6
+ self.primary_keys = %i(follower following)
7
+
8
+ belongs_to :follower_account, foreign_key: :follower, class_name: 'Account'
9
+ belongs_to :following_account, foreign_key: :following, class_name: 'Account'
10
+
11
+ scope :state, lambda { |state_name, options = {invert: false}|
12
+ state = case state_name
13
+ when :reset then 0
14
+ when :follow then 1
15
+ when :mute then 2
16
+ else; -1
17
+ end
18
+
19
+ if !!options[:invert]
20
+ where.not(state: state)
21
+ else
22
+ where(state: state)
23
+ end
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module Hive
2
+
3
+ # Tracks community members.
4
+ class Member < Base
5
+ self.table_name = :hive_members
6
+ self.primary_keys = %i(community account)
7
+
8
+ has_many :communities, primary_key: :community, foreign_key: :name
9
+ has_many :accounts, primary_key: :account, foreign_key: :name
10
+ has_many :posts, through: :communities
11
+ has_many :post_tags, through: :posts
12
+
13
+ scope :admin, lambda { |admin = true| where(is_admin: admin) }
14
+ scope :mod, lambda { |mod = true| where(is_mod: mod) }
15
+ scope :approved, lambda { |approved = true| where(is_approved: approved) }
16
+ scope :muted, lambda { |muted = true| where(is_muted: muted) }
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Hive
2
+
3
+ # Tracks modlog.
4
+ class Modlog < Base
5
+ self.table_name = :hive_modlog
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ module Hive
2
+
3
+ # Tracks the payments sent to the `null` account for Post Promotion purposes.
4
+ #
5
+ # To grab the top 100 most promoted, grouped by post promoted sum:
6
+ #
7
+ # Hive::Payment.top_amount(:post, 100)
8
+ # Hive::Payment.top_amount # same as #top_amount
9
+ class Payment < Base
10
+ self.table_name = :hive_payments
11
+
12
+ belongs_to :block, foreign_key: :block_num
13
+ belongs_to :post
14
+ belongs_to :from, foreign_key: :from_account, class_name: 'Account'
15
+ belongs_to :to, foreign_key: :to_account, class_name: 'Account'
16
+
17
+ scope :to_null, -> { where(to_account: Account.where(name: 'null')) }
18
+
19
+ scope :token, lambda { |token = 'SBD', options = {invert: false}|
20
+ if !!options[:invert]
21
+ where.not(token: token)
22
+ else
23
+ where(token: token)
24
+ end
25
+ }
26
+
27
+ scope :top_amount, lambda { |what = :post, limit = 100|
28
+ group(what).limit(limit).order('sum_amount desc').sum(:amount)
29
+ }
30
+
31
+ scope :post, lambda { |post, options = {invert: false}|
32
+ if !!options[:invert]
33
+ where.not(post: post)
34
+ else
35
+ where(post: post)
36
+ end
37
+ }
38
+ end
39
+ end
@@ -0,0 +1,422 @@
1
+ module Hive
2
+
3
+ # Tracks all content including posts, comments (replies).
4
+ #
5
+ # To find a post, you can do a lookup using the {Hive::Post#find_by_slug} scope:
6
+ #
7
+ # post = Hive::Post.find_by_slug '@steemit/firstpost'
8
+ #
9
+ # A post has many children, which are direct replies to that post.
10
+ #
11
+ # children = post.children
12
+ #
13
+ # Each child post has a parent that links back to the post it is a reply
14
+ # to:
15
+ #
16
+ # parent = children.first.parent
17
+ #
18
+ # A post also has an account_record, which is the ActiveRecord version of the
19
+ # account string:
20
+ #
21
+ # account = post.account_record
22
+ #
23
+ # A post has many followers, which are the accounts that follow the author:
24
+ #
25
+ # followers = post.followers
26
+ #
27
+ # A post belongs to a community, which can be accessed as community_record,
28
+ # the ActiveRecord version of the community string:
29
+ #
30
+ # communit = post.community_record
31
+ #
32
+ # A post has many flaggers, which are the accounts that have flagged this
33
+ # post:
34
+ #
35
+ # flaggers = post.flaggers
36
+ #
37
+ # A post has many rebloggers, which are the accounts that have reblogged this
38
+ # post:
39
+ #
40
+ # rebloggers = post.rebloggers
41
+ #
42
+ # A post has many promoters, which are the accounts that have promoted this
43
+ # post:
44
+ #
45
+ # promoters = post.promoters
46
+ #
47
+ # Scopes
48
+ # ======
49
+ #
50
+ # We can use these scopes to perform a lookup on posts:
51
+ #
52
+ # posts = Hive::Post.author 'alice'
53
+ # posts = Hive::Post.community 'steemit'
54
+ # posts = Hive::Post.category 'steemit'
55
+ # posts = Hive::Post.depth 0 # only returns root posts
56
+ # posts = Hive::Post.root_posts # same as depth 0
57
+ #
58
+ # Replies can be queried by specifying depth greater than zero:
59
+ #
60
+ # replies = Hive::Post.depth 1..10 # only returns replies up to 10 in depth
61
+ # replies = Hive::Post.depth 1..255 # only returns replies
62
+ # replies = Hive::Post.replies # same as depth 1..255
63
+ #
64
+ # We can also specify replies for a particular author, which is analogous to
65
+ # all replies to an author on steemit.com (i.e.: https://steemit.com/@alice/recent-replies):
66
+ #
67
+ # replies = Hive::Post.replies(parent_author: 'alice')
68
+ #
69
+ # We can query for posts that were reblogged by someone:
70
+ #
71
+ # posts = Hive::Post.rebloggers('alice')
72
+ #
73
+ # If we want to grab all of the posts that have *all* of these tags:
74
+ #
75
+ # posts = Hive::Post.tagged(all: %w(steemit steem video youtube))
76
+ #
77
+ # Or we can grab all of the posts with *any* of these tags:
78
+ #
79
+ # posts = Hive::Post.tagged(any: %w(steemit steem video youtube))
80
+ #
81
+ # Or, which is the same as using any:
82
+ #
83
+ # posts = Hive::Post.tagged(%w(steemit steem video youtube))
84
+ #
85
+ # Here, we can find all of the posts that mention *all* of these accounts:
86
+ #
87
+ # posts = Hive::Post.mentioned(all: %w(alice bob))
88
+ #
89
+ # Or find all of the posts that mention *any* of these accounts:
90
+ #
91
+ # posts = Hive::Post.mentioned(any: %w(alice bob))
92
+ #
93
+ # We can order by (which automatically joins {Hive::PostsCache}):
94
+ #
95
+ # posts = Hive::Post.order_by_payout(:asc)
96
+ # posts = Hive::Post.order_by_payout # same as order_by_payout(:asc)
97
+ # posts = Hive::Post.order_by_payout(:desc)
98
+ # posts = Hive::Post.order_by_children
99
+ # posts = Hive::Post.order_by_author_rep
100
+ # posts = Hive::Post.order_by_total_votes
101
+ # posts = Hive::Post.order_by_up_votes
102
+ # posts = Hive::Post.order_by_promoted
103
+ # posts = Hive::Post.order_by_created_at
104
+ # posts = Hive::Post.order_by_payout_at
105
+ # posts = Hive::Post.order_by_updated_at
106
+ # posts = Hive::Post.order_by_rshares # first result is the most flagged post!
107
+ #
108
+ # Other scopes:
109
+ #
110
+ # posts = Hive::Post.deleted
111
+ # posts = Hive::Post.deleted(false)
112
+ # posts = Hive::Post.pinned
113
+ # posts = Hive::Post.pinned(false)
114
+ # posts = Hive::Post.muted
115
+ # posts = Hive::Post.muted(false)
116
+ # posts = Hive::Post.valid
117
+ # posts = Hive::Post.valid(false)
118
+ # posts = Hive::Post.promoted
119
+ # posts = Hive::Post.promoted(false)
120
+ # posts = Hive::Post.reblogged
121
+ # posts = Hive::Post.reblogged(false)
122
+ # posts = Hive::Post.after(7.days.ago)
123
+ # posts = Hive::Post.after(7.days.ago, invert: true)
124
+ # posts = Hive::Post.before(7.days.ago)
125
+ # posts = Hive::Post.before(7.days.ago, invert: true)
126
+ # posts = Hive::Post.updated_after(7.days.ago)
127
+ # posts = Hive::Post.updated_after(7.days.ago, invert: true)
128
+ # posts = Hive::Post.updated_before(7.days.ago)
129
+ # posts = Hive::Post.updated_before(7.days.ago, invert: true)
130
+ # posts = Hive::Post.payout_after(7.days.ago)
131
+ # posts = Hive::Post.payout_after(7.days.ago, invert: true)
132
+ # posts = Hive::Post.payout_before(7.days.ago)
133
+ # posts = Hive::Post.payout_before(7.days.ago, invert: true)
134
+ # posts = Hive::Post.payout_zero
135
+ # posts = Hive::Post.payout_zero(false)
136
+ # posts = Hive::Post.app('busy')
137
+ # posts = Hive::Post.app('busy', version: '1.0')
138
+ # posts = Hive::Post.app('busy', version: '1.0', invert: true)
139
+ # posts = Hive::Post.format('markdown')
140
+ # posts = Hive::Post.format('markdown', invert: false)
141
+ #
142
+ # All scopes can be strung together, e.g.:
143
+ #
144
+ # posts = Hive::Post.root_posts.author('alice').promoted
145
+ # posts = Hive::Post.replies(parent_author: 'alice').community('steemit')
146
+ # posts = Hive::Post.category('steemit').depth(255).valid
147
+ # posts = Hive::Post.tagged(all: %w(radiator ruby)).where.not(author: 'inertia')
148
+ # posts = Hive::Post.mentioned(all: %w(inertia whatsup)).tagged(any: %w(community shoutout))
149
+ #
150
+ # Most interesting queries (once your node is fully synchronized):
151
+ #
152
+ # Hive::Post.order_by_rshares.deleted.first # o_O mfw
153
+ class Post < Base
154
+ # Blockchain limit on depth.
155
+ MAX_HARD_DEPTH = 65535
156
+ # Witness limit on depth.
157
+ MAX_SOFT_DEPTH = 255
158
+
159
+ self.table_name = :hive_posts
160
+
161
+ has_one :cache, class_name: 'PostsCache'
162
+ belongs_to :parent, class_name: 'Post'
163
+ has_many :children, -> { depth(1..MAX_SOFT_DEPTH) },
164
+ inverse_of: 'parent', foreign_key: :parent_id, class_name: 'Post'
165
+
166
+ belongs_to :author_account, primary_key: :name, foreign_key: :author, class_name: 'Account'
167
+ has_many :followers, through: :author_account, source: :followers
168
+
169
+ belongs_to :community_record, primary_key: :name, foreign_key: :community, class_name: 'Community'
170
+ has_many :community_members, through: :community_record, source: :members
171
+ has_many :community_accounts, through: :community_record, source: :accounts
172
+
173
+ has_many :flags
174
+ has_many :flaggers, through: :flags, source: :account_record
175
+ has_many :reblogs
176
+ has_many :rebloggers, through: :reblogs
177
+ has_many :post_tags
178
+
179
+ has_many :payments
180
+ has_many :promoters, through: :payments, source: :from
181
+
182
+ has_many :feed_cache
183
+ has_many :feed_accounts, through: :feed_cache, source: :account
184
+
185
+ scope :author, lambda { |author| where(author: author) }
186
+ scope :community, lambda { |community| where(community: community) }
187
+ scope :category, lambda { |category| where(category: category) }
188
+ scope :depth, lambda { |depth, options = {invert: false}|
189
+ r = if depth == 0
190
+ where(depth: depth, parent: nil)
191
+ else
192
+ where(depth: depth).where.not(parent: nil)
193
+ end
194
+
195
+ r = all.where.not(id: r) if !!options[:invert]
196
+
197
+ r
198
+ }
199
+ scope :root_posts, -> { depth(0) }
200
+ scope :replies, lambda { |options = {invert: false}|
201
+ r = depth(1..MAX_SOFT_DEPTH)
202
+
203
+ if !!options[:parent_author] || !!options[:parent_permlink]
204
+ parent_posts = all
205
+
206
+ if !!options[:parent_author]
207
+ parent_author = Post::transform_account_selector options[:parent_author]
208
+ parent_posts = parent_posts.where(author: parent_author)
209
+ end
210
+
211
+ if !!options[:parent_permlink]
212
+ parent_posts = parent_posts.where(permlink: options[:parent_permlink])
213
+ end
214
+
215
+ r = r.where(parent_id: parent_posts)
216
+ end
217
+
218
+ r = all.where.not(id: r) if !!options[:invert]
219
+
220
+ r
221
+ }
222
+ scope :deleted, lambda { |deleted = true| where(is_deleted: deleted) }
223
+ scope :pinned, lambda { |pinned = true| where(is_pinned: pinned) }
224
+ scope :muted, lambda { |muted = true| where(is_muted: muted) }
225
+ scope :valid, lambda { |valid = true| where(is_valid: valid) }
226
+ scope :promoted, lambda { |promoted = true|
227
+ if promoted
228
+ where.not(promoted: 0.0)
229
+ else
230
+ where(promoted: 0.0)
231
+ end
232
+ }
233
+ scope :reblogged, lambda { |reblogged = true|
234
+ if reblogged
235
+ where(id: Reblog.select(:post_id))
236
+ else
237
+ where.not(id: Reblog.select(:post_id))
238
+ end
239
+ }
240
+ scope :rebloggers, lambda { |rebloggers = []|
241
+ reblogs = Reblog.where(account: rebloggers)
242
+ reblogged.where(id: reblogs.select(:post_id))
243
+ }
244
+
245
+ scope :feed_cache, lambda { |account, options = {invert: false}|
246
+ r = if account.is_a? ActiveRecord::Relation
247
+ root_posts.joins(:feed_cache).
248
+ where('hive_feed_cache.account_id IN(?)', account.select(:id))
249
+ else
250
+ account = Post::transform_account_selector account
251
+ root_posts.joins(:author_account, :feed_cache).
252
+ where('hive_accounts.name IN(?)', account)
253
+ end
254
+
255
+ if !!options[:invert]
256
+ all.root_posts.where.not(id: r)
257
+ else
258
+ r
259
+ end
260
+ }
261
+
262
+
263
+ scope :feed, lambda { |account, options = {invert: false}|
264
+ account = Post::transform_account_selector account
265
+ r = root_posts.joins(:author_account, :rebloggers).
266
+ where('hive_accounts.name IN(?) OR hive_reblogs.account IN(?)', account, account)
267
+
268
+ if !!options[:invert]
269
+ all.root_posts.where.not(id: r)
270
+ else
271
+ r
272
+ end
273
+ }
274
+
275
+ scope :tagged, lambda { |*args|
276
+ options = if args[0].nil? || args[0].is_a?(Hash)
277
+ args[0] || {}
278
+ else
279
+ {any: args}
280
+ end
281
+ relation = all
282
+
283
+ if !!options[:all]
284
+ [options[:all]].flatten.map do |tag|
285
+ selector = Hive::PostTag.where(tag: tag).select(:post_id)
286
+ relation = relation.where(id: selector)
287
+ end
288
+ end
289
+
290
+ if !!options[:any]
291
+ relation = relation.where(id: Hive::PostTag.where(tag: [options[:any]].flatten).select(:post_id))
292
+ end
293
+
294
+ if !!options[:invert]
295
+ where.not(id: relation)
296
+ else
297
+ relation
298
+ end
299
+ }
300
+
301
+ scope :mentioned, lambda { |options = {}|
302
+ where(id: Hive::PostsCache.mentioned(options).select(:post_id))
303
+ }
304
+
305
+ scope :after, lambda { |after, options = {invert: false}|
306
+ invertable 'hive_posts.created_at > ?', after, options
307
+ }
308
+
309
+ scope :before, lambda { |before, options = {invert: false}|
310
+ invertable 'hive_posts.created_at < ?', before, options
311
+ }
312
+
313
+ scope :updated_after, lambda { |after, options = {invert: false}|
314
+ where(id: Hive::PostsCache.updated_after(after, options).select(:post_id))
315
+ }
316
+
317
+ scope :updated_before, lambda { |before, options = {invert: false}|
318
+ where(id: Hive::PostsCache.updated_before(before, options).select(:post_id))
319
+ }
320
+
321
+ scope :payout_after, lambda { |after, options = {invert: false}|
322
+ where(id: Hive::PostsCache.payout_after(after, options).select(:post_id))
323
+ }
324
+
325
+ scope :payout_before, lambda { |before, options = {invert: false}|
326
+ where(id: Hive::PostsCache.payout_before(before, options).select(:post_id))
327
+ }
328
+
329
+ scope :payout_zero, lambda { |payout_zero = true|
330
+ where(id: Hive::PostsCache.payout_zero(payout_zero).select(:post_id))
331
+ }
332
+
333
+ scope :app, lambda { |app, options = {version: nil, invert: false}|
334
+ where(id: Hive::PostsCache.app(app, options).select(:post_id))
335
+ }
336
+
337
+ scope :format, lambda { |format, options = {invert: false}|
338
+ where(id: Hive::PostsCache.format(format, options).select(:post_id))
339
+ }
340
+
341
+ scope :order_by_cache, lambda { |*args|
342
+ field = args[0].keys.first rescue raise('Order field must be specified.')
343
+ direction = args[0].values.first || :asc
344
+ table = Hive::PostsCache.arel_table
345
+
346
+ joins(:cache).order(table[field.to_sym].send(direction.to_sym))
347
+ }
348
+
349
+ scope :order_by_feed_cache, lambda { |*args|
350
+ field = args[0].keys.first rescue raise('Order field must be specified.')
351
+ direction = args[0].values.first || :asc
352
+ table = Hive::FeedCache.arel_table
353
+
354
+ joins(:feed_cache).order(table[field.to_sym].send(direction.to_sym))
355
+ }
356
+
357
+ scope :order_by_payout, lambda { |*args| order_by_cache(payout: args[0] || :asc) }
358
+ scope :order_by_children, lambda { |*args| order_by_cache(children: args[0] || :asc) }
359
+ scope :order_by_author_rep, lambda { |*args| order_by_cache(author_rep: args[0] || :asc) }
360
+ scope :order_by_total_votes, lambda { |*args| order_by_cache(total_votes: args[0] || :asc) }
361
+ scope :order_by_up_votes, lambda { |*args| order_by_cache(up_votes: args[0] || :asc) }
362
+ scope :order_by_promoted, lambda { |*args| order_by_cache(promoted: args[0] || :asc) }
363
+ scope :order_by_created_at, lambda { |*args| order_by_cache(created_at: args[0] || :asc) }
364
+ scope :order_by_payout_at, lambda { |*args| order_by_cache(payout_at: args[0] || :asc) }
365
+ scope :order_by_updated_at, lambda { |*args| order_by_cache(updated_at: args[0] || :asc) }
366
+ scope :order_by_rshares, lambda { |*args| order_by_cache(rshares: args[0] || :asc) }
367
+ scope :order_by_feed_cache_created_at, lambda { |*args| order_by_feed_cache(created_at: args[0] || :asc) }
368
+
369
+ # Finds a post by slug (or even URL).
370
+ #
371
+ # @param slug String A composite of author and permlink that uniquely identifies a post. E.g.: "@steemit/firstpost"
372
+ # @return <Hive::Post>
373
+ def self.find_by_slug(slug)
374
+ slug = slug.split('@').last
375
+ slug = slug.split('/')
376
+ author = slug[0]
377
+ permlink = slug[1..-1].join('/')
378
+
379
+ Post.where(author: author, permlink: permlink).first
380
+ end
381
+
382
+ # All tags related to this post.
383
+ #
384
+ # @return <String>
385
+ def tags; post_tags.pluck(:tag); end
386
+
387
+ # The entire discussion related to this post including all children and
388
+ # grandchildren replies (the result also includes this post).
389
+ #
390
+ # @return {ActiveRecord::Relation}
391
+ def discussion
392
+ clause = <<-DONE
393
+ hive_posts.community = ?
394
+ AND hive_posts.category = ?
395
+ AND hive_posts.id >= ?
396
+ AND hive_posts.id IN(?)
397
+ DONE
398
+
399
+ Post.deleted(false).
400
+ where(clause, community, category, id, discussion_ids)
401
+ end
402
+
403
+ # Returns the unique string containing the author and permlink, useful in
404
+ # restful development as param_id, if desired.
405
+ #
406
+ # @return String
407
+ def slug
408
+ "@#{author}/#{permlink}"
409
+ end
410
+ private
411
+ # @private
412
+ def discussion_ids(ids = [id])
413
+ new_ids = Post.deleted(false).depth(1..MAX_SOFT_DEPTH).
414
+ where(community: community, category: category).
415
+ where(parent_id: ids).pluck(:id)
416
+
417
+ ids += new_ids + discussion_ids(new_ids) if new_ids.any?
418
+
419
+ ids.uniq.compact
420
+ end
421
+ end
422
+ end