replacer_bot 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6c642a22ef658553bbc3f6fc8c322bda2a86745f
4
- data.tar.gz: e3fd74ac7ae4cba27a31d4cb269a971631ccbf81
3
+ metadata.gz: d9cc0084fcde5c232d3ffe37093ef1c95e1556e5
4
+ data.tar.gz: 0363fabbc0feb1e3a9b8a2fb08a06cf207120628
5
5
  SHA512:
6
- metadata.gz: 833d985c3c110abb905e5fb5020c1c36462970508a36babb94e213d7613cb4caafdd11d807ab3504b4723d0114a266c78d15e506cddef7a4cf65c5ba51c10abf
7
- data.tar.gz: 2c0444ec1bce07cc3c844e1baa9643dbfc2092bfe246610a6de38eccb746408f18603c2b81ad832b9df0c17920c8873813db543e0714f59c3f4497a706912779
6
+ metadata.gz: 506d9982aa39ac4198556ed6b42bed26d6305f117a64bccb8df617b45eb74f311e3c259a46c237dc9bcbb5dd29ab98334fa5174d24b66c1e5d1545dc235e45a5
7
+ data.tar.gz: 0e9bacffd8e373d1e05c7082d90fbbdb7f0c662e8b49eeef00ff4a96f33a88316955036c6e366aaf70ea4b1ac7af830d04f955133723ceefcd2794a64d9a7065
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ mkmf.log
15
15
 
16
16
  .env
17
17
  last.tweet
18
+ derp.rb
data/README.md CHANGED
@@ -27,7 +27,7 @@ or
27
27
 
28
28
  ## Configuration
29
29
 
30
- The default config is [here](https://github.com/pikesley/replacer_bot/blob/master/config/defaults.yml), you'll want to create your own config at `~/.replacer_bot/config.yml` to override come of these, something like:
30
+ The default config is [here](https://github.com/pikesley/replacer_bot/blob/master/config/defaults.yml), you'll want to create your own config at `~/.replacer_bot/config.yml` to override some of these, something like:
31
31
 
32
32
  search_term: David Cameron
33
33
  replacements:
@@ -35,9 +35,10 @@ The default config is [here](https://github.com/pikesley/replacer_bot/blob/maste
35
35
  - cameron: Satan
36
36
  save_file: /Users/sam/.replacer_bot/last.tweet
37
37
 
38
- Note:
38
+ Notes:
39
39
 
40
40
  * The search-and-replace terms will be applied in the order listed, which you may or may not care about
41
+ * The search part of the search-and-replace is case-insensitive
41
42
 
42
43
  You'll also need some Twitter credentials, store them in `~/.replacer_botrc` like this:
43
44
 
@@ -50,11 +51,19 @@ You'll also need some Twitter credentials, store them in `~/.replacer_botrc` lik
50
51
 
51
52
  ## Running it
52
53
 
53
- You should now be able to do run it like so:
54
+ You should now be able to run it like so:
54
55
 
55
56
  ➔ replacer tweet
56
57
  Tweeting: Satan's Little Helper sets out academy 'vision' for every school http://t.co/S6yFWRf7pD
57
58
  Sleeping 60 seconds
58
59
  Tweeting: Swarm warning: Satan's Little Helper accuses migrants of 'breaking in' to UK http://t.co/1sB5J8Alwi
59
60
 
60
- There's also a `dry_run` command, which shows you what it would have tweeted
61
+ Notes:
62
+
63
+ * Direct replies and manual retweets are excluded
64
+
65
+ There's also
66
+
67
+ ➔ replacer dry_run
68
+
69
+ which does the search and shows what it would have tweeted, without actually tweeting anything
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Coveralls::RakeTask.new
7
7
  RSpec::Core::RakeTask.new
8
8
  Cucumber::Rake::Task.new
9
9
 
10
- task :default => [:spec, 'coveralls:push']
10
+ task :default => [:spec, :cucumber, 'coveralls:push']
data/config/defaults.yml CHANGED
@@ -2,6 +2,7 @@ search_term: open data
2
2
  search_count: 100
3
3
  ignore_spaces: true
4
4
  save_file: last.tweet
5
+ seen_tweets: seen.tweets
5
6
  replacements:
6
7
  - open data: Taylor Swift
7
8
  - opendata: TaylorSwift
@@ -1,2 +1,5 @@
1
1
  require 'aruba/cucumber'
2
2
  require 'cucumber/rspec/doubles'
3
+
4
+ require 'coveralls'
5
+ Coveralls.wear!
data/lib/replacer_bot.rb CHANGED
@@ -9,6 +9,7 @@ require 'replacer_bot/version'
9
9
  require 'replacer_bot/replacer'
10
10
  require 'replacer_bot/config'
11
11
  require 'replacer_bot/helpers'
12
+ require 'replacer_bot/seen_tweets'
12
13
  require 'replacer_bot/twitter_client'
13
14
 
14
15
  Dotenv.load
@@ -1,8 +1,12 @@
1
1
  module ReplacerBot
2
- def self.encode term
2
+ def self.encode term:
3
3
  URI.encode "\"#{term}\""
4
4
  end
5
5
 
6
+ def self.is_hashtag word
7
+ word[0] == '#'
8
+ end
9
+
6
10
  def self.last_tweet
7
11
  begin
8
12
  Marshal.load File.read Config.instance.config.save_file
@@ -13,23 +17,23 @@ module ReplacerBot
13
17
  end
14
18
  end
15
19
 
16
- def self.validate string, term = Config.instance.config.search_term, ignore_spaces = true
20
+ def self.validate string:, term: Config.instance.config.search_term, ignore_spaces: true
17
21
  return false if string[0...2] == 'RT'
18
22
  return false if string[0] == '@'
19
23
 
20
24
  term = term.gsub ' ', ' ?' if ignore_spaces
21
- return true if string.index /#{term}/i
25
+ return true if string.index(/#{term}/i) && SeenTweets.validate(string)
22
26
 
23
27
  false
24
28
  end
25
29
 
26
- def self.filter list, ignore_spaces = true
27
- list.select { |i| self.validate i.text, Config.instance.config.search_term, ignore_spaces }.
30
+ def self.filter list:, ignore_spaces: true
31
+ list.select { |i| self.validate string: i.text, term: Config.instance.config.search_term, ignore_spaces: ignore_spaces }.
28
32
  select { |i| i.id > self.last_tweet}
29
33
  end
30
34
 
31
35
  def self.dehash word
32
- if word[0] == '#'
36
+ if is_hashtag word
33
37
  return word[1..-1]
34
38
  end
35
39
 
@@ -76,7 +80,7 @@ module ReplacerBot
76
80
  ]
77
81
  end
78
82
 
79
- def self.replace string, subs = Config.instance.config.replacements
83
+ def self.replace string:, subs: Config.instance.config.replacements
80
84
  # Something about a frozen string
81
85
  our_string = string.dup
82
86
  subs.each do |substitute|
@@ -10,12 +10,12 @@ module ReplacerBot
10
10
 
11
11
  def search #count = 20
12
12
  @results ||= begin
13
- results = ReplacerBot.filter @client.search(ReplacerBot.encode(@search_term), result_type: 'recent').take(@config.search_count), @config.ignore_spaces
13
+ results = ReplacerBot.filter list: @client.search(ReplacerBot.encode(term: @search_term), result_type: 'recent').take(@config.search_count), ignore_spaces: @config.ignore_spaces
14
14
  end
15
15
  end
16
16
 
17
17
  def tweets
18
- search.map { |r| ReplacerBot.truncate ReplacerBot.replace r.text }
18
+ search.map { |r| ReplacerBot.truncate ReplacerBot.replace string: r.text }
19
19
  end
20
20
 
21
21
  def tweet dry_run: false, chatty: false
@@ -0,0 +1,60 @@
1
+ module ReplacerBot
2
+ class SeenTweets
3
+ def self.validate tweet
4
+ archive = retrieve
5
+ t = sanitise tweet
6
+ valid = not(archive.include? t)
7
+ archive.add t
8
+ save archive
9
+
10
+ valid
11
+ end
12
+
13
+ def self.retrieve
14
+ begin
15
+ Marshal.load File.open Config.instance.config.seen_tweets
16
+ rescue Errno::ENOENT
17
+ Set.new
18
+ end
19
+ end
20
+
21
+ def self.clean_urls string
22
+ string.gsub /https?:\/\/[^ ]*/, '__URL__'
23
+ end
24
+
25
+ def self.hashtag_nuker string:, other_end: false
26
+ words = string.split ' '
27
+ words.reverse! if other_end
28
+
29
+ no_hashtag_yet = false
30
+
31
+ a = []
32
+ words.each do |token|
33
+ unless ReplacerBot.is_hashtag token
34
+ no_hashtag_yet = true
35
+ end
36
+
37
+ if no_hashtag_yet
38
+ a.push token
39
+ end
40
+ end
41
+
42
+ a.reverse! if other_end
43
+ a.join ' '
44
+ end
45
+
46
+ def self.nuke_hashtags string
47
+ hashtag_nuker string: (hashtag_nuker string: string, other_end: true)
48
+ end
49
+
50
+ def self.sanitise tweet
51
+ nuke_hashtags clean_urls tweet
52
+ end
53
+
54
+ def self.save set
55
+ File.open Config.instance.config.seen_tweets, 'w' do |file|
56
+ Marshal.dump set, file
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,3 @@
1
1
  module ReplacerBot
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -1,8 +1,13 @@
1
1
  module ReplacerBot
2
2
  describe 'Helpers' do
3
+ after :each do
4
+ FileUtils.rm_f Config.instance.config.save_file
5
+ FileUtils.rm_f Config.instance.config.seen_tweets
6
+ end
7
+
3
8
  context 'URLs' do
4
9
  it 'URL-encodes a search term' do
5
- expect(ReplacerBot.encode 'open data').to eq '%22open%20data%22'
10
+ expect(ReplacerBot.encode term: 'open data').to eq '%22open%20data%22'
6
11
  end
7
12
  end
8
13
 
@@ -22,23 +27,23 @@ module ReplacerBot
22
27
 
23
28
  context 'filtering' do
24
29
  it 'validates liberally' do
25
- expect(ReplacerBot.validate 'opendata hulk', 'open data').to eq true
30
+ expect(ReplacerBot.validate string: 'opendata hulk', term: 'open data').to eq true
26
31
  end
27
32
 
28
33
  it 'validates more strictly' do
29
- expect(ReplacerBot.validate 'open data ftw', 'open data', ignore_spaces = false).to eq true
30
- expect(ReplacerBot.validate 'i love opendata', 'open data', ignore_spaces = false).to eq false
34
+ expect(ReplacerBot.validate string: 'open data ftw', term: 'open data', ignore_spaces: false).to eq true
35
+ expect(ReplacerBot.validate string: 'i love opendata', term: 'open data', ignore_spaces: false).to eq false
31
36
  end
32
37
 
33
38
  it 'validates away rubbish' do
34
- expect(ReplacerBot.validate 'incredible hulk', 'open data').to eq false
39
+ expect(ReplacerBot.validate string: 'incredible hulk', term: 'open data').to eq false
35
40
  end
36
41
 
37
42
  it 'filters retweets' do
38
- expect(ReplacerBot.validate 'RT @xyz This is about Open Data').to eq false
43
+ expect(ReplacerBot.validate string: 'RT @xyz This is about Open Data').to eq false
39
44
  end
40
45
  it 'filters direct replies' do
41
- expect(ReplacerBot.validate '@abc This is a reply about Open Data').to eq false
46
+ expect(ReplacerBot.validate string: '@abc This is a reply about Open Data').to eq false
42
47
  end
43
48
  end
44
49
 
@@ -60,8 +65,8 @@ module ReplacerBot
60
65
  end
61
66
 
62
67
  it 'replaces text' do
63
- expect(ReplacerBot.replace 'Something about Open Data goes here').to eq 'Something about Taylor Swift goes here'
64
- expect(ReplacerBot.replace 'Something about #opendata http://foo.bar/').to eq 'Something about #TaylorSwift http://foo.bar/'
68
+ expect(ReplacerBot.replace string: 'Something about Open Data goes here').to eq 'Something about Taylor Swift goes here'
69
+ expect(ReplacerBot.replace string: 'Something about #opendata http://foo.bar/').to eq 'Something about #TaylorSwift http://foo.bar/'
65
70
  end
66
71
 
67
72
  it 'does a/an correctly' do
@@ -80,8 +85,13 @@ module ReplacerBot
80
85
  end
81
86
 
82
87
  it 'uses the correct article in replacements' do
83
- expect(ReplacerBot.replace 'This is an Open Data tweet').to eq 'This is a Taylor Swift tweet'
84
- expect(ReplacerBot.replace 'This is an Open Data tweet about an #opendata story').to eq 'This is a Taylor Swift tweet about a #TaylorSwift story'
88
+ expect(ReplacerBot.replace string: 'This is an Open Data tweet').to eq 'This is a Taylor Swift tweet'
89
+ expect(ReplacerBot.replace string: 'This is an Open Data tweet about an #opendata story').to eq 'This is a Taylor Swift tweet about a #TaylorSwift story'
90
+ end
91
+
92
+ it 'recognises a hashtag' do
93
+ expect(ReplacerBot.is_hashtag '#hashtag').to eq true
94
+ expect(ReplacerBot.is_hashtag 'not_hashtag').to eq false
85
95
  end
86
96
  end
87
97
  end
@@ -1,7 +1,8 @@
1
1
  module ReplacerBot
2
2
  describe Replacer do
3
3
  after :each do
4
- FileUtils.rm_f 'last.tweet'
4
+ FileUtils.rm_f Config.instance.config.save_file
5
+ FileUtils.rm_f Config.instance.config.seen_tweets
5
6
  end
6
7
 
7
8
  context 'search' do
@@ -54,18 +55,27 @@ module ReplacerBot
54
55
  end
55
56
  end
56
57
 
58
+ context 'filtering on similar tweets' do
59
+ let(:replacer) { described_class.new }
60
+
61
+ it 'filters similar tweets', :vcr do
62
+ SeenTweets.validate 'How open data can help save lives http://t.co/90U7bVq5UF'
63
+ expect(replacer.tweets.count).to eq 19
64
+ end
65
+ end
66
+
57
67
  context 'tweet' do
58
68
  let(:replacer) { described_class.new }
59
69
 
60
70
  it 'prepares sensible tweets', :vcr do
61
71
  expect(replacer.tweets).to be_a Array
62
72
  expect(replacer.tweets.first).to eq 'Taylor Swift Hackathon 6-7 октября'
63
- expect(replacer.tweets[27]).to eq 'CompTIA_SLED: RT CADeptTech: Building a User-Centric #California #Government through Demand-Driven #TaylorSwift http://t.co/mh91wbk4xK ---…'
73
+ expect(replacer.tweets[10]).to eq 'Lovely: "Does Taylor Swift Build Trust?" by @denicewross https://t.co/zcuOX6O8pA'
64
74
  expect(replacer.tweets.all? { |t| t.length <= 140} ).to eq true
65
75
  end
66
76
 
67
77
  it 'actually sends tweets', :vcr do
68
- expect(replacer.client).to(receive(:update)).exactly(21).times
78
+ expect(replacer.client).to(receive(:update)).exactly(18).times
69
79
  interval = replacer.config.interval
70
80
  replacer.config.interval = 0
71
81
  replacer.tweet
@@ -0,0 +1,96 @@
1
+ module ReplacerBot
2
+ describe SeenTweets do
3
+ after :each do
4
+ FileUtils.rm_f Config.instance.config.seen_tweets
5
+ end
6
+
7
+ context 'sanitise' do
8
+ it 'blanks out URLs' do
9
+ expect(described_class.clean_urls 'Some text with http://foo.bar/ in it').to eq 'Some text with __URL__ in it'
10
+ expect(described_class.clean_urls 'Other text with https://foo.bar/?123 and http://example.com/derp#fragment in it').to eq 'Other text with __URL__ and __URL__ in it'
11
+ expect(described_class.clean_urls 'Some text without any URLs in it').to eq 'Some text without any URLs in it'
12
+ end
13
+
14
+ it 'removes hashtags from the end of text' do
15
+ expect(described_class.nuke_hashtags 'Text finishing with a #hashtag').to eq 'Text finishing with a'
16
+ expect(described_class.nuke_hashtags 'This embedded #hashtag should survive but not this one #spurious').
17
+ to eq 'This embedded #hashtag should survive but not this one'
18
+ end
19
+
20
+ it 'removes hashtags from the beginning of text' do
21
+ expect(described_class.nuke_hashtags '#Beginning hashtag should go away').to eq 'hashtag should go away'
22
+ end
23
+
24
+ it 'strips hashtags at either end but leaves embedded ones' do
25
+ expect(described_class.nuke_hashtags '#This #will go away #but then #also #these').
26
+ to eq 'go away #but then'
27
+ end
28
+
29
+ it 'returns nothing if all it gets is hashtags' do
30
+ expect(described_class.nuke_hashtags '#nothing #but #hashtags').to eq ''
31
+ end
32
+
33
+ it 'sanitises tweets' do
34
+ expect(described_class.sanitise '#Hashtag at the start with http://derp.com/thing #this and also #these').
35
+ to eq 'at the start with __URL__ #this and also'
36
+ end
37
+ end
38
+
39
+ it 'validates the first tweet' do
40
+ expect(described_class.validate 'This is a tweet').to eq true
41
+ end
42
+
43
+ it 'invalidates on seeing the same tweet again' do
44
+ described_class.validate 'This is a tweet'
45
+ expect(described_class.validate 'This is a tweet').to eq false
46
+ end
47
+
48
+ it 'invalidates similar tweets with different URLs' do
49
+ described_class.validate 'This is a tweet with https://foo.bar/abcd'
50
+ expect(described_class.validate 'This is a tweet with https://foo.bar/xyz').to be false
51
+ end
52
+
53
+ it 'invalidates similar tweets laden with hashtags' do
54
+ described_class.validate 'This is a tweet'
55
+ expect(described_class.validate 'This is a tweet #loaded #with #hashtags').to be false
56
+ end
57
+
58
+ it 'validates and invalidates correctly' do
59
+ corpus = [
60
+ 'This is a tweet with #hashtag https://derp.com/abc #trailing #tags',
61
+ 'This is a different tweet',
62
+ '#Needless #hashtags tacked on to #this tweet'
63
+ ]
64
+ corpus.each do |tweet|
65
+ described_class.validate tweet
66
+ end
67
+
68
+ test_cases = {
69
+ 'This one should be fine' => true,
70
+ 'This is a different tweet #with #hashtags' => false,
71
+ '#Different #tags tacked on to #this tweet #here' => false,
72
+ 'This is a tweet with #hashtag http://what.even/' => false,
73
+ 'This is a tweet with #hashtag http://what.even/xyz #derp' => false
74
+ }
75
+ test_cases.each_pair do |tweet, expectation|
76
+ expect(described_class.validate tweet).to eq expectation
77
+ end
78
+
79
+ expect(described_class.retrieve.to_a.sort).to eq [
80
+ "This is a different tweet",
81
+ "This is a tweet with #hashtag __URL__",
82
+ "This one should be fine",
83
+ "tacked on to #this tweet"
84
+ ]
85
+ end
86
+
87
+ it 'saves a set' do
88
+ set = Set.new [1, 2, 3]
89
+ described_class.save set
90
+
91
+ expect(Marshal.load File.open Config.instance.config.seen_tweets).to be_a Set
92
+ expect(Marshal.load File.open Config.instance.config.seen_tweets).to include 1
93
+ expect(Marshal.load File.open Config.instance.config.seen_tweets).to include 3
94
+ end
95
+ end
96
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  require 'replacer_bot'
2
5
  require_relative 'support/vcr_setup'
3
6