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.
- checksums.yaml +4 -4
- data/bin/twitter-algebra +390 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 796f4561f08b8abd32e4e80be99be9bdfba20d77
|
4
|
+
data.tar.gz: f557e7886e21838d21004d8fe1849a47b2399479
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2375e785bdaa027a8b480f940d7d269375e4fc4d01fac60e7cd7bd2c7a7059f212b95155071eb29e1f6c2a5e711ada2b9afe9a3e55ebccba5c7fde0da6277d86
|
7
|
+
data.tar.gz: 6a7507c9ff71dbd6b6116b5dabfe4abe6a2d85d246ad4e745bfe9bd8e1ddcb0e06cb3a5d4ce58b57acba39aacb885f9335fb15a1631af016b8e25cc986b4ecbd
|
data/bin/twitter-algebra
ADDED
@@ -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.
|
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
|