twitter-vanity-suite 0.1.0 → 0.2.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.
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