twitter-vanity-suite 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/twitter-algebra +390 -0
  3. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 84f468803d3ecf4d21b55672fb369cae26690d6f
4
- data.tar.gz: 6c2f73fa13610f2b91fcfbac8efc6571cd599a58
3
+ metadata.gz: 796f4561f08b8abd32e4e80be99be9bdfba20d77
4
+ data.tar.gz: f557e7886e21838d21004d8fe1849a47b2399479
5
5
  SHA512:
6
- metadata.gz: 2ebfd25e15c003a6d33885356fbfef0c65e0df9733e5ca1fb971cb8854990b1c3a9ca4ef680ba876c54b70d4bf0948cb184e64600afaaa65f09ab005d2957efe
7
- data.tar.gz: b06c135da818f6216b763528f2b4ecd45e4b0d366eb37c51a0870053f725ad5fb8f2430a5185bcbe077a5c91d5e0e208c1742feddd559fca307a72e4b1312235
6
+ metadata.gz: 2375e785bdaa027a8b480f940d7d269375e4fc4d01fac60e7cd7bd2c7a7059f212b95155071eb29e1f6c2a5e711ada2b9afe9a3e55ebccba5c7fde0da6277d86
7
+ data.tar.gz: 6a7507c9ff71dbd6b6116b5dabfe4abe6a2d85d246ad4e745bfe9bd8e1ddcb0e06cb3a5d4ce58b57acba39aacb885f9335fb15a1631af016b8e25cc986b4ecbd
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env ruby
2
+ # Usage: twitter-algebra <expression>
3
+ #
4
+ # There are three types of sets twitter-algebra knows how to work with at the
5
+ # moment:
6
+ #
7
+ # A.following - returns the set of users @A follows on twitter
8
+ #
9
+ # A.followers - returns the set of users that follow @A on twitter
10
+ #
11
+ # {A, B, C} - returns a set containing the users @A, @B, and @C
12
+ #
13
+ # You may use the following operators in your expression:
14
+ #
15
+ # A & B - returns the intersection of sets A and B, ie. twitter users present
16
+ # in both sets
17
+ #
18
+ # A | B - returns the union of sets A and B, ie. twitter users present in
19
+ # either (or both) sets
20
+ #
21
+ # ~A - returns the complement of set A, ie. twitter users *not* present in
22
+ # the set
23
+ #
24
+ # You can combine the sets and operators described above in any way you like, for
25
+ # example:
26
+ #
27
+ # charliesome.following & ~dhh.followers
28
+ #
29
+ # - lists users @charliesome follows who do not follow @dhh
30
+ #
31
+ # charliesome.followers & (metrotrains.followers | yarratrams.followers)
32
+ #
33
+ # - lists users that follow @charliesome as well as @metrotrains or
34
+ # @yarratrams (or both!)
35
+ #
36
+ # charliesome.following | ~charliesome.following
37
+ #
38
+ # - literally every user on twitter
39
+
40
+ require_relative "../lib-internal/common"
41
+
42
+ class Token
43
+ attr_reader :type, :value
44
+
45
+ def initialize(type, value)
46
+ @type = type
47
+ @value = value
48
+ end
49
+ end
50
+
51
+ class Lexer
52
+ TOKENS = [
53
+ [:BAREWORD, /[a-z0-9_]+/],
54
+ [:DOT, /\./],
55
+ [:INTERSECTION, /&/],
56
+ [:UNION, /\|/],
57
+ [:COMPLEMENT, /~/],
58
+ [:OPEN_PAREN, /\(/],
59
+ [:CLOSE_PAREN, /\)/],
60
+ [:OPEN_BRACE, /\{/],
61
+ [:CLOSE_BRACE, /\}/],
62
+ [:COMMA, /,/],
63
+ [:WHITESPACE, /\s+/],
64
+ ]
65
+
66
+ def initialize(string)
67
+ @scanner = StringScanner.new(string)
68
+ @tokens = []
69
+ end
70
+
71
+ def lex
72
+ until scanner.eos?
73
+ tokens << next_token
74
+ end
75
+ tokens.reject! { |token| token.type == :WHITESPACE }
76
+ tokens << Token.new(:END, "")
77
+ end
78
+
79
+ private
80
+ attr_reader :scanner, :tokens
81
+
82
+ def next_token
83
+ TOKENS.each do |type, regexp|
84
+ if str = scanner.scan(regexp)
85
+ return Token.new(type, str)
86
+ end
87
+ end
88
+ raise SyntaxError, "Unexpected character: #{scanner.getch.inspect}"
89
+ end
90
+ end
91
+
92
+ module Algebra
93
+ class Set
94
+ attr_reader :values
95
+
96
+ def initialize(values)
97
+ @values = values.uniq
98
+ end
99
+
100
+ def &(other)
101
+ if other.is_a?(Complement)
102
+ Set.new(values - other.values)
103
+ else
104
+ Set.new(values & other.values)
105
+ end
106
+ end
107
+
108
+ def |(other)
109
+ if other.is_a?(Complement)
110
+ Complement.new(other.values - values)
111
+ else
112
+ Set.new(values | other.values)
113
+ end
114
+ end
115
+
116
+ def ~
117
+ Complement.new(values)
118
+ end
119
+ end
120
+
121
+ class Complement
122
+ attr_reader :values
123
+
124
+ def initialize(values)
125
+ @values = values.uniq
126
+ end
127
+
128
+ def &(other)
129
+ if other.is_a?(Complement)
130
+ Complement.new(values | other.values)
131
+ else
132
+ Complement.new(other.values - values)
133
+ end
134
+ end
135
+
136
+ def |(other)
137
+ if other.is_a?(Complement)
138
+ Complement.new(values & other.values)
139
+ else
140
+ Complement.new(values - other.values)
141
+ end
142
+ end
143
+
144
+ def ~
145
+ Set.new(values)
146
+ end
147
+ end
148
+ end
149
+
150
+ module AST
151
+ module LoadableSet
152
+ def load_data(threads)
153
+ threads[key] ||= Thread.start {
154
+ fetch_data
155
+ }
156
+ end
157
+
158
+ def evaluate(data)
159
+ Algebra::Set.new(data[key])
160
+ end
161
+ end
162
+
163
+ class Followers
164
+ include LoadableSet
165
+
166
+ attr_reader :handle
167
+
168
+ def initialize(handle)
169
+ @handle = handle
170
+ end
171
+
172
+ def fetch_data
173
+ $client.follower_ids(handle).to_a
174
+ end
175
+
176
+ def key
177
+ "#{handle}.followers"
178
+ end
179
+ end
180
+
181
+ class Following
182
+ include LoadableSet
183
+
184
+ attr_reader :handle
185
+
186
+ def initialize(handle)
187
+ @handle = handle
188
+ end
189
+
190
+ def fetch_data
191
+ $client.friend_ids(handle).to_a
192
+ end
193
+
194
+ def key
195
+ "#{handle}.following"
196
+ end
197
+ end
198
+
199
+ class HandleSet
200
+ include LoadableSet
201
+
202
+ attr_reader :handles
203
+
204
+ def initialize(handles)
205
+ @handles = handles.map(&:downcase)
206
+ end
207
+
208
+ def key
209
+ "{#{handles.sort.join(",")}}"
210
+ end
211
+
212
+ def fetch_data
213
+ $client.users(handles).map(&:id)
214
+ end
215
+ end
216
+
217
+ class Binary
218
+ attr_reader :left, :right
219
+
220
+ def initialize(left, right)
221
+ @left = left
222
+ @right = right
223
+ end
224
+
225
+ def load_data(threads)
226
+ left.load_data(threads)
227
+ right.load_data(threads)
228
+ end
229
+ end
230
+
231
+ class Intersection < Binary
232
+ def evaluate(data)
233
+ left.evaluate(data) & right.evaluate(data)
234
+ end
235
+ end
236
+
237
+ class Union < Binary
238
+ def evaluate(data)
239
+ left.evaluate(data) | right.evaluate(data)
240
+ end
241
+ end
242
+
243
+ class Complement
244
+ attr_reader :set
245
+
246
+ def initialize(set)
247
+ @set = set
248
+ end
249
+
250
+ def load_data(threads)
251
+ set.load_data(threads)
252
+ end
253
+
254
+ def evaluate(data)
255
+ ~set.evaluate(data)
256
+ end
257
+ end
258
+ end
259
+
260
+ class Parser
261
+ def initialize(tokens)
262
+ @tokens = tokens
263
+ end
264
+
265
+ def parse
266
+ expr = expression
267
+ expect_token(:END)
268
+ expr
269
+ end
270
+
271
+ private
272
+ attr_reader :tokens
273
+
274
+ def peek_token
275
+ tokens.first
276
+ end
277
+
278
+ def next_token
279
+ tokens.shift
280
+ end
281
+
282
+ def expect_token(type)
283
+ token = next_token
284
+ if token.type != type
285
+ raise SyntaxError, "Expected #{type}, saw #{token.value.inspect}"
286
+ end
287
+ token
288
+ end
289
+
290
+ def expression
291
+ binary_expression
292
+ end
293
+
294
+ BINARY_OPERATORS = {
295
+ :UNION => AST::Union,
296
+ :INTERSECTION => AST::Intersection,
297
+ }
298
+
299
+ def binary_expression
300
+ left = primary_expression
301
+ while klass = BINARY_OPERATORS[peek_token.type]
302
+ next_token
303
+ left = klass.new(left, primary_expression)
304
+ end
305
+ left
306
+ end
307
+
308
+ def primary_expression
309
+ case peek_token.type
310
+ when :COMPLEMENT
311
+ complement_expression
312
+ when :OPEN_PAREN
313
+ parenthesized_expression
314
+ when :OPEN_BRACE
315
+ handle_set_expression
316
+ when :BAREWORD
317
+ handle_expression
318
+ else
319
+ raise SyntaxError, "Unexpected #{peek_token.value.inspect}"
320
+ end
321
+ end
322
+
323
+ def complement_expression
324
+ expect_token(:COMPLEMENT)
325
+ AST::Complement.new(primary_expression)
326
+ end
327
+
328
+ def parenthesized_expression
329
+ expect_token(:OPEN_PAREN)
330
+ expr = expression
331
+ expect_token(:CLOSE_PAREN)
332
+ expr
333
+ end
334
+
335
+ COMMANDS = {
336
+ "followers" => AST::Followers,
337
+ "following" => AST::Following,
338
+ }
339
+
340
+ def handle_set_expression
341
+ expect_token(:OPEN_BRACE)
342
+ handles = []
343
+ while peek_token.type != :CLOSE_BRACE
344
+ handles << expect_token(:BAREWORD).value
345
+ break if peek_token.type == :CLOSE_BRACE
346
+ expect_token(:COMMA)
347
+ end
348
+ expect_token(:CLOSE_BRACE)
349
+ AST::HandleSet.new(handles)
350
+ end
351
+
352
+ def handle_expression
353
+ handle = expect_token(:BAREWORD).value
354
+ expect_token(:DOT)
355
+ command = expect_token(:BAREWORD).value
356
+ if klass = COMMANDS[command]
357
+ klass.new(handle)
358
+ else
359
+ raise SyntaxError, "Unknown command #{command.inspect}"
360
+ end
361
+ end
362
+ end
363
+
364
+ expr = ARGV.first
365
+ tokens = Lexer.new(expr).lex
366
+ ast = Parser.new(tokens).parse
367
+
368
+ threads = {}
369
+ data = {}
370
+ ast.load_data(threads)
371
+ threads.each do |data_name, thread|
372
+ data[data_name] = thread.value
373
+ end
374
+
375
+ result_set = ast.evaluate(data)
376
+ complement = result_set.is_a?(Algebra::Complement)
377
+
378
+ if result_set.values.empty?
379
+ # printing to stderr so we don't mess up scripts that we might be piping
380
+ # into
381
+ if complement
382
+ $stderr.puts "(everybody)"
383
+ else
384
+ $stderr.puts "(nobody)"
385
+ end
386
+ else
387
+ $client.users(result_set.values).each do |user|
388
+ puts "#{complement ? "NOT " : ""}#{user.screen_name}"
389
+ end
390
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twitter-vanity-suite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Somerville
@@ -27,6 +27,7 @@ dependencies:
27
27
  description: 'my personal set of command line twitter tools '
28
28
  email: charlie@charliesomerville.com
29
29
  executables:
30
+ - twitter-algebra
30
31
  - twitter-delete
31
32
  - twitter-hist-ids
32
33
  - twitter-intersect
@@ -34,6 +35,7 @@ executables:
34
35
  extensions: []
35
36
  extra_rdoc_files: []
36
37
  files:
38
+ - bin/twitter-algebra
37
39
  - bin/twitter-delete
38
40
  - bin/twitter-hist-ids
39
41
  - bin/twitter-intersect