coffeeoutside 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a5744ea07913c3e57048e2e18ac38db972e2f27a58412643ad5c8c21e661092
4
+ data.tar.gz: ac683194966a7ad9b8c05ed2580af6c052de9b9a937d970d07f28dc1eb833a95
5
+ SHA512:
6
+ metadata.gz: 20a68ef99969acab09abbeff6b17b869801ea5cb42e98c52376b08d8f994bd2753c545627441a54a8f96e52ec4faa67e52e991e86b73e3700a2bbe3465a2dbed
7
+ data.tar.gz: 4a439b69f7698b04d7120446e6f5a364f3ff120df35485f43c48ae88b4bb2332a067d59394939308837b15f2d1556f59fcee4ce218cddaa3ba6f597279b1431a
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.ics
2
+ /.bundle
3
+ /vendor
4
+ Gemfile.lock
5
+ config.yaml
6
+ override.yaml
7
+ prior_locations.yaml
8
+ yyc.json
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dave@dafyddcrosby.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ # TODO: submit Ruby 3.0 fixes upstream
9
+ gem 'guard'
10
+ gem 'guard-minitest'
11
+ gem 'kwalify', '= 0.7.2'
12
+ gem 'minitest'
13
+ gem 'rake'
14
+
15
+ # soon
16
+ gem 'rubocop'
17
+ # gem 'guard-rubocop'
18
+ # gem 'rubocop-minitest'
19
+ end
data/Guardfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ directories %w[lib test] \
4
+ .select { |d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist") }
5
+
6
+ guard :minitest do
7
+ watch(%r{^test/test_(.*)\.rb$}) { 'test' }
8
+ watch(%r{^lib/coffeeoutside/(.*)\.rb$}) { 'test' }
9
+ watch(%r{^lib/coffeeoutside\.rb$}) { 'test' }
10
+ watch(%r{^test/helper\.rb$}) { 'test' }
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2016-2020, David Crosby
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # CoffeeOutsideBot
2
+
3
+ The CoffeeOutsideBot is designed to pick a location in the city for the
4
+ #yycbike crowd to meet and enjoy some hot coffee (or tea!).
5
+
6
+ ## Installation
7
+
8
+ ```
9
+ git clone https://github.com/yycbike/coffeeoutsidebot.git
10
+ go build
11
+ ```
12
+
13
+ ## Twitter integration
14
+ You can get the necessary API keys at https://dev.twitter.com/
15
+
16
+ ## OpenWeatherMap integration
17
+ You can get an API key at https://openweathermap.org/price
18
+
19
+ ## iCalendar integration
20
+ An .ics file is auto generated. The current bot's version can be found at
21
+ https://coffeeoutside.bike/yyc.ics
22
+
23
+ ## Cron job
24
+ To have the coffeeoutsidebot fire regularly, set up a cron job
25
+
26
+ ```
27
+ 0 17 * * 3 pushd /path/to/coffeeoutsidebot && /path/to/coffeeoutsidebot/coffeeoutsidebot
28
+ ```
29
+
30
+ ## Contributing
31
+
32
+ Bug reports and pull requests are welcome on GitHub at
33
+ https://github.com/dafyddcrosby/coffeeoutside. This project is intended to be
34
+ a safe, welcoming space for collaboration, and contributors are expected to
35
+ adhere to the [code of
36
+ conduct](https://github.com/yycbike/coffeeoutside/blob/main/CODE_OF_CONDUCT.md).
37
+
38
+
39
+ ## License
40
+
41
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
42
+
43
+ ## Code of Conduct
44
+
45
+ Everyone interacting in the CoffeeOutside project's codebases, issue trackers,
46
+ chat rooms and mailing lists is expected to follow the [code of
47
+ conduct](https://github.com/yycbike/coffeeoutside/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: %i[test]
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'coffeeoutside'
5
+ include CoffeeOutside
6
+
7
+ CoffeeOutside.main
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'coffeeoutside'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/coffeeoutside/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'coffeeoutside'
7
+ spec.version = CoffeeOutside::VERSION
8
+ spec.authors = ['David Crosby']
9
+
10
+ spec.summary = 'The CoffeeOutside bot'
11
+ spec.description = 'The CoffeeOutside bot helps choose a coffee location based on weather and other inputs'
12
+ spec.homepage = 'https://coffeeoutside.bike'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 2.5.0'
15
+
16
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/yycbike/coffeeoutside'
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ end
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ spec.add_dependency 'icalendar', '= 2.7.1'
29
+ spec.add_dependency 'openweathermap', '= 0.2.3'
30
+ spec.add_dependency 'twitter', '= 7.0.0'
31
+ end
@@ -0,0 +1,11 @@
1
+ production: false
2
+ dispatchers:
3
+ twitter:
4
+ consumer_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
5
+ consumer_key: xxxxxxxxxxxxxxxxxxxxxxxxxx
6
+ token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
7
+ token_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
8
+ openweathermap:
9
+ city_id: 5913490
10
+ api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
11
+ units: metric
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoffeeOutside
4
+ class DispatcherBase
5
+ attr_reader :start_time, :end_time, :location, :forecast
6
+
7
+ def initialize(config)
8
+ @start_time = config[:start_time]
9
+ @end_time = config[:end_time]
10
+ @location = config[:location]
11
+ @forecast = config[:forecast]
12
+ @production = config[:production]
13
+ # Save parameters for further use by subclasses
14
+ @params = config
15
+ end
16
+
17
+ def production?
18
+ @production
19
+ end
20
+
21
+ def notify
22
+ production? ? notify_production : debug_method
23
+ end
24
+
25
+ def notify_production
26
+ raise 'notify_production must be overridden'
27
+ end
28
+
29
+ def debug_method
30
+ puts "\n"
31
+ puts self.class
32
+ notify_debug
33
+ end
34
+
35
+ def notify_debug
36
+ raise 'notify_production must be overridden'
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dispatcher'
4
+ require 'icalendar'
5
+
6
+ module CoffeeOutside
7
+ class IcalDispatcher < DispatcherBase
8
+ def generate_ical_string
9
+ format = '%Y%m%dT%H%M%S'
10
+ tzid = 'America/Edmonton'
11
+
12
+ # Create a calendar with an event (standard method)
13
+ cal = Icalendar::Calendar.new
14
+ cal.event do |e|
15
+ e.dtstart = Icalendar::Values::DateTime.new @start_time.strftime(format), 'tzid' => tzid
16
+ e.dtend = Icalendar::Values::DateTime.new @end_time.strftime(format), 'tzid' => tzid
17
+ e.summary = "CoffeeOutside - #{@location.name}"
18
+ e.location = @location.name
19
+ end
20
+ cal.to_ical
21
+ end
22
+
23
+ def notify_production
24
+ i = File.open('yyc.ics', 'w')
25
+ i.write(generate_ical_string)
26
+ end
27
+
28
+ def notify_debug
29
+ puts generate_ical_string
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dispatcher'
4
+ require 'json'
5
+
6
+ module CoffeeOutside
7
+ class JsonDispatcher < DispatcherBase
8
+ def generate_json_blob
9
+ location = {
10
+ name: @location.name,
11
+ url: @location.url
12
+ }
13
+ ::JSON.dump({ location: location })
14
+ end
15
+
16
+ def notify_production
17
+ i = File.open('yyc.json', 'w')
18
+ i.write(generate_json_blob)
19
+ end
20
+
21
+ def notify_debug
22
+ puts generate_json_blob
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: false
2
+
3
+ require_relative 'dispatcher'
4
+ require 'json'
5
+ require 'twitter'
6
+
7
+ module CoffeeOutside
8
+ class TwitterDispatcher < DispatcherBase
9
+ def notify_production
10
+ # Configure client
11
+ client = Twitter::REST::Client.new do |config|
12
+ config.consumer_key = @params['consumer_key']
13
+ config.consumer_secret = @params['consumer_secret']
14
+ config.access_token = @params['token']
15
+ config.access_token_secret = @params['token_secret']
16
+ end
17
+
18
+ # Send location tweet
19
+ t = client.update location_tweet_msg
20
+ puts t
21
+
22
+ # Send followup tweets
23
+ # client.update('test', { in_reply_to_status_id: t.id }) if t.id
24
+ end
25
+
26
+ def notify_debug
27
+ puts "consumer_key = #{@params['consumer_key']}"
28
+ puts "consumer_secret = #{@params['consumer_secret']}"
29
+ puts "access_token = #{@params['token']}"
30
+ puts "access_token_secret = #{@params['token_secret']}"
31
+ puts location_tweet_msg
32
+ puts "\n"
33
+ end
34
+
35
+ def location_tweet_msg
36
+ str = "This week's #CoffeeOutside: #{@location.name}"
37
+ str << " #{@location.url}" if @location.url
38
+ str << " (#{@location.address})" if @location.address
39
+ str << ', see you there! #yycbike'
40
+ str
41
+ end
42
+
43
+ def weather_tweet_msg
44
+ # TODO
45
+ end
46
+
47
+ def nearby_locations_tweet_msg
48
+ # TODO
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module CoffeeOutside
6
+ class Location
7
+ attr_reader :name, :address, :url, :nearby_coffee
8
+
9
+ def initialize(params)
10
+ if params['name']
11
+ @name = params['name']
12
+ else
13
+ raise 'Location class requires name key'
14
+ end
15
+ @paused = params['paused'] || false
16
+ @nearby_coffee = params['nearby_coffee'] || []
17
+ @url = params['url'] if params['url']
18
+ @address = params['address'] if params['address']
19
+
20
+ # Forecast related
21
+ @rainy_day = params['rainy_day'] || false
22
+ @high_limit = params['high_limit'] if params['high_limit']
23
+ @low_limit = params['low_limit'] if params['low_limit']
24
+
25
+ # Save params for any dispatcher-specific values
26
+ @params = params
27
+ end
28
+
29
+ def paused?
30
+ @paused
31
+ end
32
+
33
+ def weather_appropriate?(forecast)
34
+ # TODO: stderr reasons?
35
+ if forecast.rainy? && !@rainy_day
36
+ return false
37
+ elsif @low_limit && (forecast.temperature < @low_limit)
38
+ return false
39
+ elsif @high_limit && (forecast.temperature > @high_limit)
40
+ return false
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ def to_s
47
+ @name
48
+ end
49
+ end
50
+
51
+ class LocationFile
52
+ attr_reader :locations
53
+
54
+ def initialize(filename = './locations.yaml')
55
+ y = YAML.load_file(filename)
56
+ @locations = []
57
+ y.each do |l|
58
+ @locations.append Location.new(l)
59
+ end
60
+ @locations
61
+ end
62
+ end
63
+
64
+ class OverrideFile
65
+ attr_reader :location
66
+
67
+ def initialize(filename = './override.yaml')
68
+ @filename = filename
69
+ if ::File.exist? @filename
70
+ @override = true
71
+ @location = Location.new(YAML.load_file(filename))
72
+ else
73
+ @override = false
74
+ @location = nil
75
+ end
76
+ end
77
+
78
+ def override?
79
+ @override
80
+ end
81
+
82
+ def delete_file
83
+ ::File.delete @filename
84
+ end
85
+ end
86
+
87
+ class LocationChooser
88
+ attr_reader :location
89
+
90
+ def initialize(destructive = false, forecast)
91
+ @location = nil
92
+ of = OverrideFile.new
93
+ plf = PriorLocationsFile.new
94
+
95
+ # First check override file
96
+ if of.override?
97
+ @location = of.location
98
+ of.delete_file if destructive
99
+ else
100
+ # If no override location, determine one
101
+ locations = LocationFile.new.locations
102
+
103
+ # Remove paused locations
104
+ locations.delete_if(&:paused?)
105
+
106
+ # Remove previously selected locations
107
+ prior_locations = plf.previous_locations
108
+ locations.delete_if { |l| prior_locations.include? l.name }
109
+
110
+ # Delete locations that don't meet forecast criteria
111
+ locations.keep_if { |l| l.weather_appropriate? forecast }
112
+
113
+ # Raise if no locations remaining
114
+ raise 'No locations remaining!' if locations.empty?
115
+
116
+ # Pick random location
117
+ @location = locations.sample
118
+ end
119
+
120
+ # Append to prior locations list
121
+ plf.append_location @location if destructive
122
+
123
+ @location
124
+ end
125
+ end
126
+
127
+ class PriorLocationsFile
128
+ def initialize(filename = './prior_locations.yaml')
129
+ @filename = filename
130
+ @locations = YAML.load_file(filename) || []
131
+ end
132
+
133
+ def previous_locations(n = 5)
134
+ @locations.last n
135
+ end
136
+
137
+ def append_location(location)
138
+ @locations.append location.name
139
+ f = File.open(@filename, 'w')
140
+ f.write(YAML.dump(@locations))
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoffeeOutside
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openweathermap'
4
+
5
+ module CoffeeOutside
6
+ class OWM
7
+ def initialize(config)
8
+ @city_id = config['city_id']
9
+ @api_key = config['api_key']
10
+
11
+ get_forecast
12
+ end
13
+
14
+ def api_call
15
+ api = OpenWeatherMap::API.new(@api_key, 'en', 'metric')
16
+ api.forecast(@city_id)
17
+ end
18
+
19
+ def get_forecast
20
+ # TODO: this looks wrong, check @time!
21
+ fc = api_call.forecast[2]
22
+ Forecast.new(humidity: fc.humidity, temperature: fc.temperature)
23
+ end
24
+ end
25
+
26
+ HUMIDITY_LIMIT = 75
27
+ class Forecast
28
+ def initialize(hash)
29
+ @humidity = hash[:humidity] || 0
30
+ @temperature = hash[:temperature] || 0
31
+ end
32
+
33
+ def rainy?
34
+ # TODO: could also regex for "rain" or "snow" from OWM...
35
+ @humidity >= HUMIDITY_LIMIT
36
+ end
37
+
38
+ attr_reader :temperature
39
+
40
+ def to_s
41
+ "Forecast is temp of #{@temperature} humidity #{@humidity}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'date'
5
+ require_relative 'coffeeoutside/version'
6
+ require_relative 'coffeeoutside/locations'
7
+ require_relative 'coffeeoutside/weather'
8
+
9
+ # Dispatchers
10
+ require_relative 'coffeeoutside/dispatchers/json'
11
+ require_relative 'coffeeoutside/dispatchers/ical'
12
+ require_relative 'coffeeoutside/dispatchers/twitter'
13
+
14
+ module CoffeeOutside
15
+ class Error < StandardError; end
16
+
17
+ class Config
18
+ attr_reader :dispatchers, :openweathermap
19
+
20
+ def initialize(config_file = 'config.yaml')
21
+ config = YAML.load_file(config_file)
22
+ @production = config['production']
23
+ @dispatchers = config['dispatchers']
24
+ @openweathermap = config['openweathermap']
25
+ end
26
+
27
+ def production?
28
+ @production
29
+ end
30
+ end
31
+
32
+ def next_friday
33
+ # TODO: this is gross...
34
+ @next_friday ||= Date.today + [5, 4, 3, 2, 1, 7, 6][Date.today.wday]
35
+ end
36
+
37
+ def get_start_time
38
+ DateTime.new(
39
+ next_friday.year, next_friday.month, next_friday.day,
40
+ 7, 30, 0
41
+ )
42
+ end
43
+
44
+ def get_end_time
45
+ DateTime.new(
46
+ next_friday.year, next_friday.month, next_friday.day,
47
+ 8, 30, 0
48
+ )
49
+ end
50
+
51
+ def main
52
+ config = Config.new
53
+ if config.production?
54
+ puts config.openweathermap
55
+ owm = OWM.new config.openweathermap
56
+ forecast = owm.get_forecast
57
+ else
58
+ forecast = Forecast.new(humidity: 0, temperature: 10)
59
+ end
60
+
61
+ destructive = config.production?
62
+ location = LocationChooser.new(destructive, forecast).location
63
+ puts "Chosen location is #{location}"
64
+
65
+ dispatch = {
66
+ start_time: get_start_time,
67
+ end_time: get_end_time,
68
+ forecast: forecast,
69
+ location: location,
70
+ production: config.production?
71
+ }
72
+ JsonDispatcher.new(dispatch).notify
73
+ IcalDispatcher.new(dispatch).notify
74
+ TwitterDispatcher.new(dispatch.merge(config.dispatchers['twitter'])).notify
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ desc: CoffeeOutside location schema
2
+ type: seq
3
+ sequence:
4
+ - type: map
5
+ mapping:
6
+ "name":
7
+ desc: "Name of the meetup location"
8
+ type: str
9
+ required: yes
10
+ "url":
11
+ desc: "URL for the site. Order of preference: City of Calgary link (if park), Cafe website (if inside), Google Maps URL"
12
+ type: str
13
+ "address":
14
+ desc: "Physical address for the location"
15
+ type: str
16
+ "nearby_coffee":
17
+ desc: "Nearby places to purchase coffee"
18
+ type: str
19
+ "paused":
20
+ desc: "True if site is paused from being used"
21
+ type: bool
22
+ "rainy_day":
23
+ desc: "True if site is adequate for a rainy day"
24
+ type: bool
25
+ "low_limit":
26
+ desc: "The lowest temperature that the site would be comfortable for"
27
+ type: int
28
+ range: { min: -50, max: 50 }
29
+ "high_limit":
30
+ type: int
31
+ desc: "The highest temperature that the site would be comfortable for (more suited for indoor locations)"
32
+ range: { min: -50, max: 50 }
data/locations.yaml ADDED
@@ -0,0 +1,213 @@
1
+ ---
2
+ - name: "@LaBoulangerie4"
3
+ address: 2435 4 St SW
4
+ high_limit: 5
5
+ rainy_day: true
6
+ paused: true
7
+ - high_limit: 5
8
+ name: "@SidewalkSimmons"
9
+ url: http://sidewalkcitizenbakery.com/
10
+ rainy_day: true
11
+ paused: true
12
+ - url: https://alforno.ca/
13
+ name: "@AlfornoYYC"
14
+ high_limit: 5
15
+ rainy_day: true
16
+ paused: true
17
+ - url: https://iloveyoucoffeeshop.com/
18
+ name: "@iloveyoucoffee_"
19
+ high_limit: 5
20
+ rainy_day: true
21
+ paused: true
22
+ - url: http://www.bayaricacafe.ca/
23
+ name: "@BayaRicaCafe"
24
+ address: 204 7A Street NE
25
+ high_limit: 5
26
+ rainy_day: true
27
+ paused: true
28
+ - url: https://www.calgaryheritageroastingco.com/
29
+ name: Calgary Heritage Roasting Company
30
+ address: 2020 11 St SE
31
+ high_limit: 5
32
+ rainy_day: true
33
+ paused: true
34
+ - address: 420 2nd Street SW
35
+ high_limit: 0
36
+ name: "@monogramco"
37
+ rainy_day: true
38
+ - name: Barb Scott Park
39
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Barb-Scott-Park.aspx
40
+ low_limit: -15
41
+ - name: Buckmaster Park
42
+ low_limit: 10
43
+ address: 1629 21 Ave SW
44
+ - high_limit: 0
45
+ address: 909 10 St SE
46
+ name: "@cafegravity"
47
+ rainy_day: true
48
+ paused: true
49
+ - high_limit: -15
50
+ address: 1613 9th Street SW
51
+ name: Caffe Beano
52
+ rainy_day: true
53
+ paused: false
54
+ - name: Ca'puccini
55
+ paused: true
56
+ - name: CNIB Fragrant Garden
57
+ address: 10 11A St NE
58
+ low_limit: 5
59
+ url: TODO
60
+ paused: true
61
+ - name: Central Memorial Park
62
+ low_limit: -15
63
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Central-Memorial-Park.aspx
64
+ - name: Connaught Park
65
+ low_limit: -15
66
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Connaught-Park.aspx
67
+ - name: Containr Park
68
+ address: TODO
69
+ low_limit: -15
70
+ url: TODO
71
+ paused: true
72
+ - name: East Village Music Pavilion
73
+ url: https://www.evexperience.com/music-pavilion
74
+ low_limit: -10
75
+ rainy_day: true
76
+ - name: Delta Park
77
+ url: https://www.google.com/maps/place/Delta+Garden/@51.0521029,-114.0786464,17z/
78
+ low_limit: -10
79
+ rainy_day: false
80
+ - name: DeVille Luxury Coffee
81
+ paused: true
82
+ - name: Enmax Stage on Prince's Island
83
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Princes-Island-Park.aspx
84
+ low_limit: -10
85
+ rainy_day: true
86
+ - name: Gerry Shaw Gardens
87
+ low_limit: 10
88
+ address: Elbow Drive & 30th Ave SW
89
+ - name: Harley Hotchkiss Gardens
90
+ low_limit: -5
91
+ address: 611 4 St SW
92
+ - name: Harvie Passage (Weir)
93
+ low_limit: 0
94
+ url: https://www.google.ca/maps/place/Harvie+Passage,+Calgary,+AB/@51.0439438,-114.0137959,15z/data=!4m2!3m1!1s0x53717ac80a6f6a1b:0xe7a331ec29495195
95
+ - name: Haultain Park
96
+ low_limit: -15
97
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Haultain-Park.aspx
98
+ - name: High Park (walk your bike up)
99
+ low_limit: 0
100
+ url: https://www.beltlineyyc.ca/highparkyyc
101
+ - name: Horsy Park
102
+ low_limit: 10
103
+ url: https://goo.gl/maps/ido1KAcqdJGtkeVN8
104
+ - name: Humpy Hollow Park
105
+ url: https://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Humpy-Hollow-Park.aspx
106
+ low_limit: 10
107
+ - name: Ice Rink at Olympic Plaza
108
+ high_limit: -2
109
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Olympic-Plaza.aspx
110
+ low_limit: -15
111
+ nearby_coffee: Ca'Puccini
112
+ - name: James Short Park
113
+ low_limit: -5
114
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/James-Short-Park.aspx
115
+ - name: Lougheed House Beaulieu Gardens
116
+ low_limit: -5
117
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Beaulieu-Gardens.aspx
118
+ - name: McHugh Bluff
119
+ low_limit: 0
120
+ url: TODO
121
+ paused: true
122
+ - name: Munro Park
123
+ url: TODO
124
+ paused: true
125
+ - low_limit: 10
126
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Olympic-Plaza.aspx
127
+ name: Olympic Plaza
128
+ - url: http://calgary.ca/CSPS/Parks/Pages/Locations/NW-parks/Riley-Park.aspx
129
+ low_limit: 0
130
+ name: Patrick Burns Rock Garden at Riley Park
131
+ - name: Peace Park
132
+ low_limit: 0
133
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Peace-Park.aspx
134
+ - name: Poetic Park Plaza
135
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Poetic-Park.aspx
136
+ low_limit: 0
137
+ - low_limit: 3
138
+ url: http://www.calgaryplus.ca/calgary/venues/pumphouse-park
139
+ name: Pumphouse Park
140
+ - name: Reader Rock Garden
141
+ low_limit: 10
142
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/SE-parks/Reader-Rock-Garden.aspx
143
+ - name: Riley Park (Stage)
144
+ url: https://www.calgary.ca/csps/parks/locations/nw-parks/riley-park.html
145
+ low_limit: -10
146
+ rainy_day: true
147
+ - name: Rosso Coffee Roasters
148
+ paused: true
149
+ - low_limit: 5
150
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/NE-parks/Rotary-Park.aspx
151
+ name: Rotary Park
152
+ - name: Rundle Ruins
153
+ address: 632 13 Avenue SE
154
+ low_limit: 0
155
+ - low_limit: 0
156
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/SW-parks/Sandy-Beach.aspx
157
+ name: Sandy Beach
158
+ - url: https://goo.gl/maps/yzMjfBj4JrKQxkvu6
159
+ low_limit: 0
160
+ name: South Mount Royal Park
161
+ - url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Shaw-Millennium-Park.aspx
162
+ low_limit: 0
163
+ name: Shaw Millenium Park
164
+ - url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Sien-Lok-Park.aspx
165
+ low_limit: 0
166
+ name: Sien Lok Park
167
+ - name: The Roasterie
168
+ high_limit: 5
169
+ address: 314 10th St NW
170
+ rainy_day: true
171
+ paused: true
172
+ - name: Societe Coffee Lounge
173
+ high_limit: -3
174
+ address: 1223 11 Ave SW
175
+ rainy_day: true
176
+ paused: true
177
+ - name: Sought X Found Coffee
178
+ url: https://www.soughtxfound.coffee
179
+ high_limit: -3
180
+ address: 916 Centre Street North
181
+ rainy_day: true
182
+ paused: true
183
+ - low_limit: 10
184
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/SW-parks/Stanley-Park.aspx
185
+ name: Stanley Park Flower Garden
186
+ - name: Sunalta Community Wildflower Garden
187
+ low_limit: 7
188
+ address: 1310 16 St SW
189
+ - name: The Rise on St. Patrick's Island
190
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/St.-Patrick's-Island-Park.aspx
191
+ low_limit: 0
192
+ - name: Confluence Plaza on St. Patrick's Island
193
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/St.-Patrick's-Island-Park.aspx
194
+ low_limit: -5
195
+ rainy_day: true
196
+ - name: Thomson Family Park
197
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Thomson-Family-Park.aspx
198
+ low_limit: 0
199
+ rainy_day: true
200
+ - name: Tom Campbell's Hill
201
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/NE-parks/Tom-Campbells-Hill.aspx
202
+ low_limit: 0
203
+ - name: Tomkins Park
204
+ url: https://www.calgary.ca/CSPS/Parks/Pages/Locations/Downtown-parks/Tomkins-Park.aspx
205
+ low_limit: -5
206
+ rainy_day: true
207
+ - low_limit: -10
208
+ url: https://www.google.ca/maps/place/McDougall+Centre,+Calgary,+AB+T2P/@51.049273,-114.0773586,17z/data=!4m2!3m1!1s0x53716fe4fd339a6f:0x6a0dd3ff806e1e98
209
+ name: West Side of McDougall Centre
210
+ - low_limit: 0
211
+ address: 215 24 Ave SW
212
+ url: https://goo.gl/maps/mNg4SaM1DeBX9V3L6
213
+ name: William Aberhart Park
@@ -0,0 +1,3 @@
1
+ ---
2
+ name: Tom Campbell's Hill
3
+ url: http://www.calgary.ca/CSPS/Parks/Pages/Locations/NE-parks/Tom-Campbells-Hill.aspx
@@ -0,0 +1,15 @@
1
+ type: map
2
+ mapping:
3
+ "name":
4
+ desc: "Name of the meetup location"
5
+ type: str
6
+ required: yes
7
+ "url":
8
+ desc: "URL for the site. Order of preference: City of Calgary link (if park), Cafe website (if inside), Google Maps URL"
9
+ type: str
10
+ "address":
11
+ desc: "Physical address for the location"
12
+ type: str
13
+ "nearby_coffee":
14
+ desc: "Nearby places to purchase coffee"
15
+ type: str
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coffeeoutside
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Crosby
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-05-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: icalendar
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.7.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.7.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: openweathermap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: twitter
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 7.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 7.0.0
55
+ description: The CoffeeOutside bot helps choose a coffee location based on weather
56
+ and other inputs
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - CODE_OF_CONDUCT.md
64
+ - Gemfile
65
+ - Guardfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/coffeeoutsidebot
70
+ - bin/console
71
+ - bin/setup
72
+ - coffeeoutside.gemspec
73
+ - config.example.yaml
74
+ - lib/coffeeoutside.rb
75
+ - lib/coffeeoutside/dispatchers/dispatcher.rb
76
+ - lib/coffeeoutside/dispatchers/ical.rb
77
+ - lib/coffeeoutside/dispatchers/json.rb
78
+ - lib/coffeeoutside/dispatchers/twitter.rb
79
+ - lib/coffeeoutside/locations.rb
80
+ - lib/coffeeoutside/version.rb
81
+ - lib/coffeeoutside/weather.rb
82
+ - locations.schema.yaml
83
+ - locations.yaml
84
+ - override.example.yaml
85
+ - override.schema.yaml
86
+ homepage: https://coffeeoutside.bike
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ allowed_push_host: https://rubygems.org
91
+ homepage_uri: https://coffeeoutside.bike
92
+ source_code_uri: https://github.com/yycbike/coffeeoutside
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.5.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.2.22
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: The CoffeeOutside bot
112
+ test_files: []