├── docs └── screenshots │ ├── help.jpg │ ├── cta_tweet.jpg │ ├── help_commands.jpg │ ├── snowbot_features.jpg │ ├── snowbot_features.png │ ├── snowbot_profile.jpg │ └── welcome_message.jpg ├── app ├── config │ ├── data │ │ ├── photos │ │ │ ├── corduroy.jpg │ │ │ ├── east_pela.JPG │ │ │ ├── snow_bike.jpg │ │ │ ├── snow_geese.JPG │ │ │ ├── snow_tree.jpg │ │ │ ├── east_pela_2.JPG │ │ │ ├── arapahoe_pass.jpg │ │ │ ├── heavy_as_stone.jpg │ │ │ ├── horseshoe_bowl.jpg │ │ │ ├── loveland_tracks.jpg │ │ │ ├── wear_them_out_1.jpg │ │ │ ├── wear_them_out_2.jpg │ │ │ ├── loveland_tracks_2.jpg │ │ │ ├── start_em_young_1.jpg │ │ │ ├── eldora_pillow_snow.jpg │ │ │ ├── peak_10_closing_time.jpg │ │ │ ├── all_the_factors_at_scale.jpg │ │ │ ├── loveland_above_timberline.jpg │ │ │ ├── 127430D2-0731-4499-BCC3-A87A4A7FD759.JPG │ │ │ ├── 1CCA4FF4-0259-40EE-864A-2CB9DF7CA4C1.JPG │ │ │ ├── 21AE7CC9-1997-4919-8151-BAE7464BFE72.JPG │ │ │ ├── 22896A8B-5C9E-4748-A007-19CEC458720B.JPG │ │ │ ├── 344428B1-D823-41B1-90A2-084DB0C7618A.JPG │ │ │ ├── 3E114A36-FE41-473C-810F-70F12824F301.JPG │ │ │ ├── 3EAA035A-ED97-4044-8766-4BD6E5F1611E.JPG │ │ │ ├── 4073BB61-B799-4B6C-A78B-2A01D1189810.JPG │ │ │ ├── 4F189C9A-FD9B-4B9B-84CA-136E1EFE2FBA.JPG │ │ │ ├── 60ECE635-4113-4B1B-9A58-36CDE0F274AE.JPG │ │ │ ├── 64B20E75-F6D6-4B7A-9EC7-F260B85A40E1.JPG │ │ │ ├── 6BFA3292-6FBB-4E32-B741-87F37C840E49.JPG │ │ │ ├── 6E442580-5C03-4466-83DA-C462DCB2B963.JPG │ │ │ ├── 6E9C5310-5A65-4540-BB65-92A3ABF12562.JPG │ │ │ ├── 73CBDF62-55FF-4221-932A-265B31A07CC9.JPG │ │ │ ├── 751BBD76-7480-44DB-B87E-947756F5399C.JPG │ │ │ ├── 77119DE0-52DF-40C2-AA52-B90FE11222E4.JPG │ │ │ ├── 90EF7C66-9130-4F0D-BC61-69B80A151B90.JPG │ │ │ ├── 97612E59-4BEF-4714-A56E-11179069878C.JPG │ │ │ ├── 9BFA386E-C098-4D14-946B-F7AA4C587B05.JPG │ │ │ ├── AAABBDEE-F543-4CB4-879C-FC33D62D569F.JPG │ │ │ ├── AB9BF7DF-859A-4D06-8797-401583ADCD98.JPG │ │ │ ├── AC92ECAB-13D1-4A25-A8FF-53E55E66D2D1.JPG │ │ │ ├── AE88A33B-C80D-4D15-A5D7-0FC404DA3C4E.JPG │ │ │ ├── BC67117E-CDF0-438B-A92E-D73F2CEB2C8E.JPG │ │ │ ├── C3222A02-A2F5-4D75-A36B-044AA0EDBC43.JPG │ │ │ ├── CA52F922-7388-4FF0-9FFE-CF8D6C83E384.JPG │ │ │ ├── E0214D0E-F1B8-4D18-90A8-2F84B7BE8090.JPG │ │ │ ├── EEB595B0-8F4D-4995-B5B9-8850D8C81A45.JPG │ │ │ ├── F1DE20DB-2F1E-4BE7-B6F3-4690BD5E1473.JPG │ │ │ ├── F1E08D09-754D-4949-962C-B00FB79E8117.JPG │ │ │ ├── F494FEBC-B189-4E75-9038-8E87B791B21E.JPG │ │ │ ├── F8215F2F-4906-4E8C-87C5-943128EABB6E.JPG │ │ │ ├── F96AB650-9676-4803-BE3C-87787D51A945.JPG │ │ │ ├── photos_skipped.csv │ │ │ └── photos.csv │ │ ├── locations │ │ │ └── placesOfInterest.csv │ │ ├── music │ │ │ └── playlists.csv │ │ └── links │ │ │ └── links.csv │ └── environment.rb ├── helpers │ ├── twitter_api.rb │ ├── send_direct_message.rb │ ├── api_oauth_request.rb │ ├── get_resources.rb │ ├── third_party_request.rb │ ├── event_manager.rb │ └── generate_direct_message_content.rb └── controllers │ └── snow_bot_dev_app.rb ├── Gemfile ├── config.ru ├── README.md ├── LICENSE └── scripts ├── setup_welcome_messages.rb └── setup_webhooks.rb /docs/screenshots/help.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/help.jpg -------------------------------------------------------------------------------- /docs/screenshots/cta_tweet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/cta_tweet.jpg -------------------------------------------------------------------------------- /docs/screenshots/help_commands.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/help_commands.jpg -------------------------------------------------------------------------------- /app/config/data/photos/corduroy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/corduroy.jpg -------------------------------------------------------------------------------- /app/config/data/photos/east_pela.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/east_pela.JPG -------------------------------------------------------------------------------- /app/config/data/photos/snow_bike.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/snow_bike.jpg -------------------------------------------------------------------------------- /app/config/data/photos/snow_geese.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/snow_geese.JPG -------------------------------------------------------------------------------- /app/config/data/photos/snow_tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/snow_tree.jpg -------------------------------------------------------------------------------- /docs/screenshots/snowbot_features.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/snowbot_features.jpg -------------------------------------------------------------------------------- /docs/screenshots/snowbot_features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/snowbot_features.png -------------------------------------------------------------------------------- /docs/screenshots/snowbot_profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/snowbot_profile.jpg -------------------------------------------------------------------------------- /docs/screenshots/welcome_message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/docs/screenshots/welcome_message.jpg -------------------------------------------------------------------------------- /app/config/data/photos/east_pela_2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/east_pela_2.JPG -------------------------------------------------------------------------------- /app/config/data/photos/arapahoe_pass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/arapahoe_pass.jpg -------------------------------------------------------------------------------- /app/config/data/photos/heavy_as_stone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/heavy_as_stone.jpg -------------------------------------------------------------------------------- /app/config/data/photos/horseshoe_bowl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/horseshoe_bowl.jpg -------------------------------------------------------------------------------- /app/config/data/photos/loveland_tracks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/loveland_tracks.jpg -------------------------------------------------------------------------------- /app/config/data/photos/wear_them_out_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/wear_them_out_1.jpg -------------------------------------------------------------------------------- /app/config/data/photos/wear_them_out_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/wear_them_out_2.jpg -------------------------------------------------------------------------------- /app/config/data/photos/loveland_tracks_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/loveland_tracks_2.jpg -------------------------------------------------------------------------------- /app/config/data/photos/start_em_young_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/start_em_young_1.jpg -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'oauth' 4 | gem 'sinatra', require: 'sinatra/base' 5 | gem 'shotgun' 6 | gem 'twitter' 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/config/data/photos/eldora_pillow_snow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/eldora_pillow_snow.jpg -------------------------------------------------------------------------------- /app/config/data/photos/peak_10_closing_time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/peak_10_closing_time.jpg -------------------------------------------------------------------------------- /app/config/data/photos/all_the_factors_at_scale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/all_the_factors_at_scale.jpg -------------------------------------------------------------------------------- /app/config/data/photos/loveland_above_timberline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/loveland_above_timberline.jpg -------------------------------------------------------------------------------- /app/config/data/photos/127430D2-0731-4499-BCC3-A87A4A7FD759.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/127430D2-0731-4499-BCC3-A87A4A7FD759.JPG -------------------------------------------------------------------------------- /app/config/data/photos/1CCA4FF4-0259-40EE-864A-2CB9DF7CA4C1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/1CCA4FF4-0259-40EE-864A-2CB9DF7CA4C1.JPG -------------------------------------------------------------------------------- /app/config/data/photos/21AE7CC9-1997-4919-8151-BAE7464BFE72.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/21AE7CC9-1997-4919-8151-BAE7464BFE72.JPG -------------------------------------------------------------------------------- /app/config/data/photos/22896A8B-5C9E-4748-A007-19CEC458720B.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/22896A8B-5C9E-4748-A007-19CEC458720B.JPG -------------------------------------------------------------------------------- /app/config/data/photos/344428B1-D823-41B1-90A2-084DB0C7618A.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/344428B1-D823-41B1-90A2-084DB0C7618A.JPG -------------------------------------------------------------------------------- /app/config/data/photos/3E114A36-FE41-473C-810F-70F12824F301.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/3E114A36-FE41-473C-810F-70F12824F301.JPG -------------------------------------------------------------------------------- /app/config/data/photos/3EAA035A-ED97-4044-8766-4BD6E5F1611E.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/3EAA035A-ED97-4044-8766-4BD6E5F1611E.JPG -------------------------------------------------------------------------------- /app/config/data/photos/4073BB61-B799-4B6C-A78B-2A01D1189810.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/4073BB61-B799-4B6C-A78B-2A01D1189810.JPG -------------------------------------------------------------------------------- /app/config/data/photos/4F189C9A-FD9B-4B9B-84CA-136E1EFE2FBA.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/4F189C9A-FD9B-4B9B-84CA-136E1EFE2FBA.JPG -------------------------------------------------------------------------------- /app/config/data/photos/60ECE635-4113-4B1B-9A58-36CDE0F274AE.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/60ECE635-4113-4B1B-9A58-36CDE0F274AE.JPG -------------------------------------------------------------------------------- /app/config/data/photos/64B20E75-F6D6-4B7A-9EC7-F260B85A40E1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/64B20E75-F6D6-4B7A-9EC7-F260B85A40E1.JPG -------------------------------------------------------------------------------- /app/config/data/photos/6BFA3292-6FBB-4E32-B741-87F37C840E49.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/6BFA3292-6FBB-4E32-B741-87F37C840E49.JPG -------------------------------------------------------------------------------- /app/config/data/photos/6E442580-5C03-4466-83DA-C462DCB2B963.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/6E442580-5C03-4466-83DA-C462DCB2B963.JPG -------------------------------------------------------------------------------- /app/config/data/photos/6E9C5310-5A65-4540-BB65-92A3ABF12562.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/6E9C5310-5A65-4540-BB65-92A3ABF12562.JPG -------------------------------------------------------------------------------- /app/config/data/photos/73CBDF62-55FF-4221-932A-265B31A07CC9.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/73CBDF62-55FF-4221-932A-265B31A07CC9.JPG -------------------------------------------------------------------------------- /app/config/data/photos/751BBD76-7480-44DB-B87E-947756F5399C.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/751BBD76-7480-44DB-B87E-947756F5399C.JPG -------------------------------------------------------------------------------- /app/config/data/photos/77119DE0-52DF-40C2-AA52-B90FE11222E4.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/77119DE0-52DF-40C2-AA52-B90FE11222E4.JPG -------------------------------------------------------------------------------- /app/config/data/photos/90EF7C66-9130-4F0D-BC61-69B80A151B90.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/90EF7C66-9130-4F0D-BC61-69B80A151B90.JPG -------------------------------------------------------------------------------- /app/config/data/photos/97612E59-4BEF-4714-A56E-11179069878C.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/97612E59-4BEF-4714-A56E-11179069878C.JPG -------------------------------------------------------------------------------- /app/config/data/photos/9BFA386E-C098-4D14-946B-F7AA4C587B05.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/9BFA386E-C098-4D14-946B-F7AA4C587B05.JPG -------------------------------------------------------------------------------- /app/config/data/photos/AAABBDEE-F543-4CB4-879C-FC33D62D569F.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/AAABBDEE-F543-4CB4-879C-FC33D62D569F.JPG -------------------------------------------------------------------------------- /app/config/data/photos/AB9BF7DF-859A-4D06-8797-401583ADCD98.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/AB9BF7DF-859A-4D06-8797-401583ADCD98.JPG -------------------------------------------------------------------------------- /app/config/data/photos/AC92ECAB-13D1-4A25-A8FF-53E55E66D2D1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/AC92ECAB-13D1-4A25-A8FF-53E55E66D2D1.JPG -------------------------------------------------------------------------------- /app/config/data/photos/AE88A33B-C80D-4D15-A5D7-0FC404DA3C4E.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/AE88A33B-C80D-4D15-A5D7-0FC404DA3C4E.JPG -------------------------------------------------------------------------------- /app/config/data/photos/BC67117E-CDF0-438B-A92E-D73F2CEB2C8E.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/BC67117E-CDF0-438B-A92E-D73F2CEB2C8E.JPG -------------------------------------------------------------------------------- /app/config/data/photos/C3222A02-A2F5-4D75-A36B-044AA0EDBC43.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/C3222A02-A2F5-4D75-A36B-044AA0EDBC43.JPG -------------------------------------------------------------------------------- /app/config/data/photos/CA52F922-7388-4FF0-9FFE-CF8D6C83E384.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/CA52F922-7388-4FF0-9FFE-CF8D6C83E384.JPG -------------------------------------------------------------------------------- /app/config/data/photos/E0214D0E-F1B8-4D18-90A8-2F84B7BE8090.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/E0214D0E-F1B8-4D18-90A8-2F84B7BE8090.JPG -------------------------------------------------------------------------------- /app/config/data/photos/EEB595B0-8F4D-4995-B5B9-8850D8C81A45.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/EEB595B0-8F4D-4995-B5B9-8850D8C81A45.JPG -------------------------------------------------------------------------------- /app/config/data/photos/F1DE20DB-2F1E-4BE7-B6F3-4690BD5E1473.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/F1DE20DB-2F1E-4BE7-B6F3-4690BD5E1473.JPG -------------------------------------------------------------------------------- /app/config/data/photos/F1E08D09-754D-4949-962C-B00FB79E8117.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/F1E08D09-754D-4949-962C-B00FB79E8117.JPG -------------------------------------------------------------------------------- /app/config/data/photos/F494FEBC-B189-4E75-9038-8E87B791B21E.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/F494FEBC-B189-4E75-9038-8E87B791B21E.JPG -------------------------------------------------------------------------------- /app/config/data/photos/F8215F2F-4906-4E8C-87C5-943128EABB6E.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/F8215F2F-4906-4E8C-87C5-943128EABB6E.JPG -------------------------------------------------------------------------------- /app/config/data/photos/F96AB650-9676-4803-BE3C-87787D51A945.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdevplatform/SnowBotDev/HEAD/app/config/data/photos/F96AB650-9676-4803-BE3C-87787D51A945.JPG -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require 3 | 4 | #require_relative './app/config/environment.rb' 5 | require File.expand_path('../app/config/environment', __FILE__) 6 | 7 | run SnowBotDevApp 8 | -------------------------------------------------------------------------------- /app/config/environment.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require 3 | 4 | # get the path of the root of the app 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | # require the controller(s) 8 | Dir.glob(File.join(APP_ROOT, 'controllers', '*.rb')).each { |file| require file } 9 | 10 | # require the helper(s) 11 | Dir.glob(File.join(APP_ROOT, 'helpers', '*.rb')).each { |file| require file } 12 | 13 | # require database configurations 14 | #require File.join(APP_ROOT, 'config', 'database') 15 | 16 | # configure TaskManagerApp settings 17 | class Application < Sinatra::Base 18 | set :method_override, true 19 | set :root, APP_ROOT 20 | 21 | #puts "APP_ROOT: #{APP_ROOT}" 22 | 23 | #set :views, File.join(APP_ROOT, "app", "views") 24 | #set :public_folder, File.join(APP_ROOT, "app", "public") 25 | end 26 | -------------------------------------------------------------------------------- /app/config/data/locations/placesOfInterest.csv: -------------------------------------------------------------------------------- 1 | #http://feeds.snocountry.net/getResortList.php?apiKey=KEY_ID&states=co&resortType=alpine&output=json 2 | Arapahoe Basin, -105.8717, 39.6423, 303001 3 | Aspen, -106.8175, 39.1911, 303003 4 | Beaver Creek,,,303005 5 | Breckenridge, -106.0384, 39.4817, 303007 6 | Copper Mountain, -106.1516, 39.5014, 303009 7 | Crested Butte, -106.9878, 38.8697,303010 8 | Eldora, -105.5639, 39.9486, 303011 9 | Keystone, -105.9347, 39.5792,303014 10 | Loveland,,,303015 11 | Snowmass,,,303020 12 | Steamboat, -106.8317, 40.4850,303021 13 | Squaw Valley CA,,,916011 14 | Telluride, -107.8123, 37.9375,303022 15 | Vail, -106.3742, 39.640,303023 16 | Winter Park, -105.7631, 39.8917, 303024 17 | Wolf Creek, -106.8021, 7.4827, 303025 18 | Coronet Peak NZ,,,4424660 19 | The Remarkables NZ,,,615013 20 | Afton Alps MN, -92.7907, 44.8545,612002 21 | #Buckhill MN, -93.2873, 44.7231, 612004 -------------------------------------------------------------------------------- /app/config/data/music/playlists.csv: -------------------------------------------------------------------------------- 1 | Rain songs,Songs about the rain,https://open.spotify.com/user/jeem8/playlist/41L74udeD0c4OUoTvJw27r 2 | Snow songs,Songs about 'snow' (or at least mention 'snow'),https://open.spotify.com/user/jeem8/playlist/4rE9XrERcLIQbrtHMbvpkk 3 | New Zealand Intro (◉ Easiest),Essential Kiwi bands that everyone should know,https://open.spotify.com/user/jeem8/playlist/1sY1SNC1eeRH1qZsp8XIZf 4 | New Zealand Intro (▩ Intermediate),More good stuff,https://open.spotify.com/user/jeem8/playlist/0qqB6QBa2ClvA7opLC9BL4 5 | New Zealand Intro (◈ Expert),On to the next level,https://open.spotify.com/user/jeem8/playlist/2ruHknVzXgrU4awDIYwpA0 6 | Land of Lakes - MN (◉ Easiest),Something in the water?,https://open.spotify.com/user/jeem8/playlist/78Y2b43lYVbb3PF3vciQFb 7 | Land of Lakes - MN (◈ Expert),Sorry mom - forgot to take out the thrash,https://open.spotify.com/user/jeem8/playlist/6C4VG4b0k2FKnPFWBFPscA 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @SnowBotDev 2 | Current home of the [@SnowBotDev](https://twitter.com/SnowBotDev), a demo illustrating the Twitter Account Activity and Direct Message APIs. This SnowBot is written in Ruby, uses the Sinatra web app framework, and is deployed on Heroku. 3 | 4 | --------------------- 5 | #### *If you want to check out the bot, [send a Direct Message to @SnowBotDev](https://twitter.com/messages/compose?recipient_id=906948460078698496)...* 6 | --------------------- 7 | 8 | See [HERE](https://github.com/twitterdev/SnowBotDev/wiki) for a tutorial on building the SnowBot... 9 | 10 | --------------------- 11 | 12 | 13 |  14 | 15 | --------------------- 16 | Snowbot features: 17 | --------------------- 18 |  19 | --------------------- 20 | Support commands: 21 | --------------------- 22 |  23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 @snowman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/helpers/twitter_api.rb: -------------------------------------------------------------------------------- 1 | require 'twitter' #Opens doors to the rest of the standard Twitter APIs. 2 | #https://github.com/sferik/twitter/blob/master/examples/Configuration.md 3 | 4 | class TwitterAPI 5 | 6 | attr_accessor :keys, 7 | :upload_client, 8 | :base_url, # 'https://api.twitter.com/' or 'upload.twitter.com' or ? 9 | :uri_path #No default. 10 | 11 | def initialize() 12 | 13 | #puts "Creating Twitter (public) API object." 14 | 15 | @base_url = 'upload.twitter.com' 16 | @uri_path = '/1.1/media/upload' 17 | 18 | #Get Twitter App keys and tokens. Read from 'config.yaml' if provided, or if running on Heroku, pull from the 19 | #'Config Variables' via the ENV{} hash. 20 | @keys = {} 21 | 22 | @keys['consumer_key'] = ENV['CONSUMER_KEY'] 23 | @keys['consumer_secret'] = ENV['CONSUMER_SECRET'] 24 | @keys['access_token'] = ENV['ACCESS_TOKEN'] 25 | @keys['access_token_secret'] = ENV['ACCESS_TOKEN_SECRET'] 26 | 27 | @upload_client = Twitter::REST::Client.new(@keys) 28 | 29 | end 30 | 31 | def get_media_id(media_path) 32 | 33 | #puts "Value of media: #{media_path}" 34 | 35 | media_id = nil 36 | 37 | if media_path != '' and not media_path.nil? 38 | puts "Calling upload with #{media_path}" 39 | media_id = @upload_client.upload(File.new(media_path)) 40 | else 41 | media_id = nil 42 | end 43 | 44 | media_id 45 | 46 | end 47 | 48 | end 49 | 50 | -------------------------------------------------------------------------------- /app/controllers/snow_bot_dev_app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'base64' 3 | require 'json' 4 | 5 | require_relative "../../app/helpers/event_manager" 6 | 7 | class SnowBotDevApp < Sinatra::Base 8 | 9 | def initialize 10 | puts "Starting up web app." 11 | super() 12 | end 13 | 14 | #Load authentication details 15 | keys = {} 16 | set :dm_api_consumer_secret, ENV['CONSUMER_SECRET'] #Account Activity API with OAuth 17 | 18 | set :title, 'snowbotdev' 19 | 20 | def generate_crc_response(consumer_secret, crc_token) 21 | hash = OpenSSL::HMAC.digest('sha256', consumer_secret, crc_token) 22 | return Base64.encode64(hash).strip! 23 | end 24 | 25 | get '/' do 26 | '
Welcome to snowbotdev, home of the @SnowBotDev system...
27 |I am a sinatra-based web app...
28 |I consume Twitter Account Activity webhook events and manage DM bot dialogs...
29 |I serve a local hive of snow photos, weather conditions, snow reports, snow research links...
30 | 31 | 32 |#ThinkSnow...
' 33 | end 34 | 35 | # Receives challenge response check (CRC). 36 | get '/snowbotdev' do 37 | crc_token = params['crc_token'] 38 | 39 | if not crc_token.nil? 40 | 41 | #puts "CRC event with #{crc_token}" 42 | #puts "headers: #{headers}" 43 | #puts headers['X-Twitter-Webhooks-Signature'] 44 | 45 | response = {} 46 | response['response_token'] = "sha256=#{generate_crc_response(settings.dm_api_consumer_secret, crc_token)}" 47 | 48 | body response.to_json 49 | end 50 | 51 | status 200 52 | 53 | end 54 | 55 | # Receives DM events. 56 | post '/snowbotdev' do 57 | #puts "Received event(s) from DM API" 58 | request.body.rewind 59 | events = request.body.read 60 | 61 | manager = EventManager.new 62 | manager.handle_event(events) 63 | 64 | status 200 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/config/data/photos/photos_skipped.csv: -------------------------------------------------------------------------------- 1 | corduroy.jpg; One of my favorites. After a hearty breakfast and cup of coffee, an early warm-up run at Breckenridge. 2 | 0BB73AAD-CA67-4F20-B00F-E180AEFBB48E.JPG; Late season at Mary Jane. 3 | 127430D2-0731-4499-BCC3-A87A4A7FD759.JPG; Spring skiing in the back of Copper Mountain. 4 | 1CCA4FF4-0259-40EE-864A-2CB9DF7CA4C1.JPG; View from @TwitterBoulder. 5 | 21AE7CC9-1997-4919-8151-BAE7464BFE72.JPG; The pool under snow... 6 | 22896A8B-5C9E-4748-A007-19CEC458720B.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 7 | 344428B1-D823-41B1-90A2-084DB0C7618A.JPG; Late season at Araphoe Basin. 8 | 3E114A36-FE41-473C-810F-70F12824F301.JPG; Looking toward KT-22, Squaw Valley. June 2017. 9 | 3EAA035A-ED97-4044-8766-4BD6E5F1611E.JPG; Great day at Vail with colleagues. 10 | 4073BB61-B799-4B6C-A78B-2A01D1189810.JPG; The Swing Tree, McIntosh Lake, Longmont, CO 11 | 4F189C9A-FD9B-4B9B-84CA-136E1EFE2FBA.JPG; California Snow Survey site... Instrument and sensor heaven. June 2017. 12 | 4F3D270F-D825-4AC5-8367-E02B649689AD.JPG; Last day with 2017 beard. 13 | 5A780C26-8D36-4EC7-BEA2-5A5CF09AA3F8.JPG; BOO! 14 | 60ECE635-4113-4B1B-9A58-36CDE0F274AE.JPG; Snow day in Longmont, CO. 15 | 64B20E75-F6D6-4B7A-9EC7-F260B85A40E1.JPG; Top of Squaw Valley, June 2017. 16 | 6BFA3292-6FBB-4E32-B741-87F37C840E49.JPG; Chinese New Year greeting, McIntosh Lake, Longmont, CO. 17 | 6E442580-5C03-4466-83DA-C462DCB2B963.JPG; Continental Cabin at Mohawk Lakes, Summit County, CO (live from frig). 18 | 6E9C5310-5A65-4540-BB65-92A3ABF12562.JPG; Pasture at McIntosh Lake, Longmont CO. 19 | 73CBDF62-55FF-4221-932A-265B31A07CC9.JPG; Top of Peak 10, Breckenridge, on a powder day. 20 | 751BBD76-7480-44DB-B87E-947756F5399C.JPG; Arapahoe Basin, late season. 21 | 77119DE0-52DF-40C2-AA52-B90FE11222E4.JPG; Snow day in Longmont, CO. 22 | 7B2609B5-21F9-41A3-B27B-16C53C7EB245.JPG; Winter hike, McIntosh Lake, Longmont, CO. 23 | 90EF7C66-9130-4F0D-BC61-69B80A151B90.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 24 | 97612E59-4BEF-4714-A56E-11179069878C.JPG; Crystal Peak from out back at Copper Mountain. Spring 2017. 25 | 9BFA386E-C098-4D14-946B-F7AA4C587B05.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 26 | AAABBDEE-F543-4CB4-879C-FC33D62D569F.JPG; Mary Jane. 27 | AB9BF7DF-859A-4D06-8797-401583ADCD98.JPG; Copper Mountain, Spring 2017. 28 | AC92ECAB-13D1-4A25-A8FF-53E55E66D2D1.JPG; Mary Jane. 29 | AE88A33B-C80D-4D15-A5D7-0FC404DA3C4E.JPG; Spring pond at Arapahoe Basin, Spring 2017. 30 | B2CBF9A9-D0B3-4812-8F06-D17278010FF1.JPG; Flying over some ski area... 31 | BC67117E-CDF0-438B-A92E-D73F2CEB2C8E.JPG; Top of Squaw Valley, June 2017. 32 | C3222A02-A2F5-4D75-A36B-044AA0EDBC43.JPG; Top of Mary Jane, Spring 2017. 33 | CA52F922-7388-4FF0-9FFE-CF8D6C83E384.JPG; Snow day in Longmont, CO. 34 | CE96A9AE-AC7B-46D8-B109-90D7621B6149.JPG; Late season at Loveland? A-Basin? 35 | E0214D0E-F1B8-4D18-90A8-2F84B7BE8090.JPG; The Swing Tree, McIntosh Lake, Longmont, CO 36 | EEB595B0-8F4D-4995-B5B9-8850D8C81A45.JPG; View of KT-22 from Squaw Valley tram, June 2017. 37 | F19C0685-A17B-457C-B6EA-03A6858D3360.JPG; Arapahoe Basin in all its chalet deck glory. 38 | F1DE20DB-2F1E-4BE7-B6F3-4690BD5E1473.JPG; The back of Copper Mountain... April 2017. 39 | F1E08D09-754D-4949-962C-B00FB79E8117.JPG; On the deck, December 2015. 40 | F494FEBC-B189-4E75-9038-8E87B791B21E.JPG; In memory of a lost friend, former colleague... Boarding with Greg was a hoot. 41 | F8215F2F-4906-4E8C-87C5-943128EABB6E.JPG; Looking south from top of Mary Jane, Spring 2017. 42 | F96AB650-9676-4803-BE3C-87787D51A945.JPG; Late season, Arapahoe Basin. 43 | -------------------------------------------------------------------------------- /app/config/data/photos/photos.csv: -------------------------------------------------------------------------------- 1 | corduroy.jpg; One of my favorites. After a hearty breakfast and cup of coffee, an early warm-up run at Breckenridge. 2 | 0BB73AAD-CA67-4F20-B00F-E180AEFBB48E.JPG; Late season at Mary Jane. 3 | 127430D2-0731-4499-BCC3-A87A4A7FD759.JPG; Spring skiing in the back of Copper Mountain. 4 | 1CCA4FF4-0259-40EE-864A-2CB9DF7CA4C1.JPG; View from @TwitterBoulder. 5 | 21AE7CC9-1997-4919-8151-BAE7464BFE72.JPG; The pool under snow... 6 | 22896A8B-5C9E-4748-A007-19CEC458720B.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 7 | 344428B1-D823-41B1-90A2-084DB0C7618A.JPG; Late season at Araphoe Basin. 8 | 3E114A36-FE41-473C-810F-70F12824F301.JPG; Looking toward KT-22, Squaw Valley. June 2017. 9 | 3EAA035A-ED97-4044-8766-4BD6E5F1611E.JPG; Great day at Vail with colleagues. 10 | 4073BB61-B799-4B6C-A78B-2A01D1189810.JPG; The Swing Tree, McIntosh Lake, Longmont, CO 11 | 4F189C9A-FD9B-4B9B-84CA-136E1EFE2FBA.JPG; California Snow Survey site... Instrument and sensor heaven. June 2017. 12 | 60ECE635-4113-4B1B-9A58-36CDE0F274AE.JPG; Snow day in Longmont, CO. 13 | 64B20E75-F6D6-4B7A-9EC7-F260B85A40E1.JPG; Top of Squaw Valley, June 2017. 14 | 6BFA3292-6FBB-4E32-B741-87F37C840E49.JPG; Chinese New Year greeting, McIntosh Lake, Longmont, CO. 15 | 6E442580-5C03-4466-83DA-C462DCB2B963.JPG; Continental Cabin at Mohawk Lakes, Summit County, CO (live from frig). 16 | 6E9C5310-5A65-4540-BB65-92A3ABF12562.JPG; Pasture at McIntosh Lake, Longmont CO. 17 | 73CBDF62-55FF-4221-932A-265B31A07CC9.JPG; Top of Peak 10, Breckenridge, on a powder day. 18 | 751BBD76-7480-44DB-B87E-947756F5399C.JPG; Arapahoe Basin, late season. 19 | 77119DE0-52DF-40C2-AA52-B90FE11222E4.JPG; Snow day in Longmont, CO. 20 | 90EF7C66-9130-4F0D-BC61-69B80A151B90.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 21 | 97612E59-4BEF-4714-A56E-11179069878C.JPG; Crystal Peak from out back at Copper Mountain. Spring 2017. 22 | 9BFA386E-C098-4D14-946B-F7AA4C587B05.JPG; Central Sierra Snow Lab... Near Donner Pass. June 2017. 23 | AAABBDEE-F543-4CB4-879C-FC33D62D569F.JPG; Looking south from near the top of Mary Jane. 24 | AB9BF7DF-859A-4D06-8797-401583ADCD98.JPG; Copper Mountain, Spring 2017. 25 | AC92ECAB-13D1-4A25-A8FF-53E55E66D2D1.JPG; Enjoying a rest on an old school Mary Jane lift. March 2017. 26 | AE88A33B-C80D-4D15-A5D7-0FC404DA3C4E.JPG; Spring pond at Arapahoe Basin, Spring 2017. 27 | BC67117E-CDF0-438B-A92E-D73F2CEB2C8E.JPG; Top of Squaw Valley, June 2017. 28 | C3222A02-A2F5-4D75-A36B-044AA0EDBC43.JPG; Top of Mary Jane, Spring 2017. 29 | CA52F922-7388-4FF0-9FFE-CF8D6C83E384.JPG; Snow day in Longmont, CO. 30 | E0214D0E-F1B8-4D18-90A8-2F84B7BE8090.JPG; The Swing Tree, McIntosh Lake, Longmont, CO 31 | EEB595B0-8F4D-4995-B5B9-8850D8C81A45.JPG; View of KT-22 from Squaw Valley tram, June 2017. 32 | F1DE20DB-2F1E-4BE7-B6F3-4690BD5E1473.JPG; The back of Copper Mountain... April 2017. 33 | F1E08D09-754D-4949-962C-B00FB79E8117.JPG; On the deck, December 2015. 34 | F494FEBC-B189-4E75-9038-8E87B791B21E.JPG; In memory of a lost friend, former colleague... Boarding with Greg was a hoot. 35 | F8215F2F-4906-4E8C-87C5-943128EABB6E.JPG; Looking south from top of Mary Jane, Spring 2017. 36 | F96AB650-9676-4803-BE3C-87787D51A945.JPG; Late season, Arapahoe Basin. April 2016. 37 | all_the_factors_at_scale.jpg; Lake McIntosh snow 38 | arapahoe_pass.jpg; Looking south from west ramp up to the pass. 39 | eldora_pillow_snow.jpg; Eldora on a powder day. March 2012 40 | heavy_as_stone.jpg; Heaviest, most over-engineered ski/binding combo ever. Never again. 41 | horseshoe_bowl.jpg; 12,000 feet, 2,400 feet above Breckenridge. 42 | loveland_above_timberline.jpg; If you've never been to Loveland, you should go. 43 | loveland_tracks.jpg; Loveland tracks. 44 | loveland_tracks_2.jpg; Loveland tracks. 45 | peak_10_closing_time.jpg; Closing time on Peak 10, Breckenridge. 46 | snow_bike.jpg; Snow bike. 47 | snow_tree.jpg; Snow tree. 48 | start_em_young_1.jpg; No fears on a soft powder day. 49 | wear_them_out_1.jpg; Another big day at Breckenridge. 50 | wear_them_out_2.jpg; Another big day at Breckenridge. 51 | snow_geese.JPG; Snow geese, Pela Crossing, Hygiene, CO. 52 | east_pela.JPG; East Pela Crossing, Hygiene, CO. 53 | east_pela_2.JPG; East Pela Crossing, Hygiene, CO. -------------------------------------------------------------------------------- /app/helpers/send_direct_message.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'csv' 3 | require 'pathname' 4 | require_relative 'api_oauth_request' 5 | require_relative 'generate_direct_message_content' 6 | 7 | class SendDirectMessage 8 | 9 | attr_accessor :dm, #Object that manages DM API requests. 10 | :content, 11 | :sender 12 | 13 | def initialize 14 | 15 | #puts "Creating SendDirectMessage object." 16 | 17 | @dm = ApiOauthRequest.new 18 | 19 | @dm.uri_path = '/1.1/direct_messages' 20 | @dm.get_api_access 21 | 22 | @content = GenerateDirectMessageContent.new 23 | 24 | end 25 | 26 | =begin 27 | #Not implemented yet. 28 | def send_snow_day(recipient_id) 29 | #Demonstrates easy way to stub out future functionality until customer 'generate content' method is written. 30 | dm_content = @content.generate_snow_day(recipient_id) 31 | send_direct_message(dm_content) 32 | end 33 | =end 34 | 35 | def send_photo(recipient_id) 36 | dm_content = @content.generate_random_photo(recipient_id) 37 | send_direct_message(dm_content) 38 | end 39 | 40 | #Saved for when we have a workaround for getting user location coordinates. 41 | def send_weather_info(recipient_id, coordinates) 42 | dm_content = @content.generate_weather_info(recipient_id, coordinates) 43 | send_direct_message(dm_content) 44 | end 45 | 46 | #Links are list based, and currently app has just one links list. 47 | def send_links_list(recipient_id) 48 | dm_content = @content.generate_link_list(recipient_id) 49 | send_direct_message(dm_content) 50 | end 51 | 52 | def send_link(recipient_id, choice) 53 | dm_content = @content.generate_link(recipient_id, choice) 54 | send_direct_message(dm_content) 55 | end 56 | 57 | #Snow reports are list based, and currently app has just one location list. 58 | def send_locations_list(recipient_id) 59 | dm_content = @content.generate_location_list(recipient_id) 60 | send_direct_message(dm_content) 61 | end 62 | 63 | def send_location_info(recipient_id, choice) 64 | dm_content = @content.generate_location_info(recipient_id, choice) 65 | send_direct_message(dm_content) 66 | end 67 | 68 | #Links are list based, and currently app has just one links list. 69 | def send_playlists_list(recipient_id) 70 | dm_content = @content.generate_playlist_list(recipient_id) 71 | send_direct_message(dm_content) 72 | end 73 | 74 | def send_playlist(recipient_id, choice) 75 | dm_content = @content.generate_playlist(recipient_id, choice) 76 | send_direct_message(dm_content) 77 | end 78 | 79 | # App Generic? All apps have these by default? 80 | 81 | def send_system_info(recipient_id) 82 | dm_content = @content.generate_system_info(recipient_id) 83 | send_direct_message(dm_content) 84 | end 85 | 86 | def send_system_help(recipient_id) 87 | dm_content = @content.generate_system_help(recipient_id) 88 | send_direct_message(dm_content) 89 | end 90 | 91 | def send_welcome_message(recipient_id) 92 | dm_content = @content.generate_welcome_message(recipient_id) 93 | send_direct_message(dm_content) 94 | end 95 | 96 | def send_custom_message(recipient_id, message) 97 | dm_content = @content.generate_message(recipient_id, message) 98 | send_direct_message(dm_content) 99 | end 100 | 101 | #Send a DM back to user. 102 | #https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event 103 | def send_direct_message(message) 104 | 105 | uri_path = "#{@dm.uri_path}/events/new.json" 106 | response = @dm.make_post_request(uri_path, message) 107 | 108 | #Currently, not returning anything... Errors reported in POST request code. 109 | response 110 | 111 | end 112 | 113 | end 114 | 115 | #And here you can unit test sending different types of DMs... send map? attach media? 116 | 117 | if __FILE__ == $0 #This script code is executed when running this file. 118 | 119 | sender = SendDirectMessage.new 120 | #sender.send_map(944480690) 121 | #sender.send_photo(944480690) 122 | 123 | #sender.send_links_list(944480690) 124 | #sender.respond_with_link(944480690,'NASA') 125 | 126 | sender.send_location_info(944480690,"The Remarkables NZ") 127 | 128 | end -------------------------------------------------------------------------------- /app/helpers/api_oauth_request.rb: -------------------------------------------------------------------------------- 1 | #With many Twitter (Public) APIs, you can just use something like the 'twitter' gem. 2 | #This example instead builds requests making the 'oauth' gem, and is not Twitter specific. 3 | 4 | require 'json' 5 | require 'oauth' 6 | require 'yaml' 7 | 8 | class ApiOauthRequest 9 | 10 | HEADERS = {"content-type" => "application/json"} #Suggested set? Any? 11 | 12 | attr_accessor :keys, 13 | :twitter_api, 14 | :base_url #Default: 'https://api.twitter.com/' 15 | 16 | 17 | def initialize(config=nil) 18 | 19 | @base_url = 'https://api.twitter.com' 20 | 21 | #'Config Variables' via the ENV{} hash. 22 | @keys = {} 23 | 24 | if config.nil? 25 | #Load keys from ENV. 26 | @keys['consumer_key'] = ENV['CONSUMER_KEY'] 27 | @keys['consumer_secret'] = ENV['CONSUMER_SECRET'] 28 | @keys['access_token'] = ENV['ACCESS_TOKEN'] 29 | @keys['access_token_secret'] = ENV['ACCESS_TOKEN_SECRET'] 30 | else 31 | #Load from config file. 32 | @keys = YAML::load_file(config) 33 | end 34 | 35 | end 36 | 37 | #API client object is created with the @base_url context, then individual requests are made with specific URI paths passed in. 38 | 39 | def get_api_access 40 | consumer = OAuth::Consumer.new(@keys['consumer_key'], @keys['consumer_secret'], {:site => @base_url}) 41 | token = {:oauth_token => @keys['access_token'], 42 | :oauth_token_secret => @keys['access_token_secret'] 43 | } 44 | 45 | @twitter_api = OAuth::AccessToken.from_hash(consumer, token) 46 | 47 | end 48 | 49 | def make_post_request(uri_path, request) 50 | get_api_access if @twitter_api.nil? #token timeout? 51 | 52 | response = @twitter_api.post(uri_path, request, HEADERS) 53 | 54 | if response.code.to_i >= 300 55 | puts "POST ERROR occurred with #{uri_path}, request: #{request} " 56 | puts "Error code: #{response.code} #{response}" 57 | puts "Error Message: #{response.body}" 58 | end 59 | 60 | if response.body.nil? #Some successful API calls have nil response bodies, but have 2## response codes. 61 | return response.code #Examples include 'set subscription', 'get subscription', and 'delete subscription' 62 | else 63 | return response.body 64 | end 65 | 66 | end 67 | 68 | def make_get_request(uri_path) 69 | get_api_access if @twitter_api.nil? #token timeout? 70 | 71 | response = @twitter_api.get(uri_path, HEADERS) 72 | 73 | if response.code.to_i >= 300 74 | puts "GET ERROR occurred with #{uri_path}: " 75 | puts "Error: #{response}" 76 | end 77 | 78 | if response.body.nil? #Some successful API calls have nil response bodies, but have 2## response codes. 79 | return response.code #Examples include 'set subscription', 'get subscription', and 'delete subscription' 80 | else 81 | return response.body 82 | end 83 | end 84 | 85 | def make_delete_request(uri_path) 86 | get_api_access if @twitter_api.nil? #token timeout? 87 | 88 | response = @twitter_api.delete(uri_path, HEADERS) 89 | 90 | if response.code.to_i >= 300 91 | puts "DELETE ERROR occurred with #{uri_path}: " 92 | puts "Error: #{response}" 93 | end 94 | 95 | if response.body.nil? #Some successful API calls have nil response bodies, but have 2## response codes. 96 | return response.code #Examples include 'set subscription', 'get subscription', and 'delete subscription' 97 | else 98 | return response.body 99 | end 100 | end 101 | 102 | def make_put_request(uri_path) 103 | 104 | get_api_access if @twitter_api.nil? #token timeout? 105 | 106 | response = @twitter_api.put(uri_path, '', {"content-type" => "application/json"}) 107 | 108 | if response.code.to_i == 429 109 | puts "#{response.message} - Rate limited..." 110 | end 111 | 112 | if response.code.to_i >= 300 113 | puts "PUT ERROR occurred with #{uri_path}, request: #{request} " 114 | puts "Error: #{response}" 115 | end 116 | 117 | if response.body.nil? #Some successful API calls have nil response bodies, but have 2## response codes. 118 | return response.code #Examples include 'set subscription', 'get subscription', and 'delete subscription' 119 | else 120 | return response.body 121 | end 122 | 123 | end 124 | 125 | end -------------------------------------------------------------------------------- /app/config/data/links/links.csv: -------------------------------------------------------------------------------- 1 | CU INSTAAR;Institute of Arctic and Alpine Research;https://instaar.colorado.edu/;INSTAAR develops scientific knowledge of physical and biogeochemical environmental processes at local, regional and global scales, and applies this knowledge to improve society's awareness and understanding of natural and anthropogenic environmental change. 2 | CO Avalanche info;Colorado Avalanche Information Center;http://http://avalanche.state.co.us/;The Colorado Avalanche Information Center (CAIC) is a program within the Colorado Department of Natural Resources, Executive Director’s Office. The mission of the CAIC is to provide avalanche information, education and promote research for the protection of life, property and the enhancement of the state’s economy. Since 1950 avalanches have killed more people in Colorado than any other natural hazard, and in the United States, Colorado accounts for one-third of all avalanche deaths. The Colorado Avalanche Warning Center began issuing public avalanche forecasts in 1973 as part of a research program in the USDA-Forest Service Rocky Mountain Research Station. The program moved out of the federal government and into the Colorado state government, becoming part of the Department of Natural Resources in 1983. 3 | NSIDC;National Snow & Ice Data Center;https://nsidc.org/cryosphere/snow/science;The National Snow and Ice Data Center (NSIDC) supports research into our world’s frozen realms: the snow, ice, glaciers, frozen ground, and climate interactions that make up Earth’s cryosphere. NSIDC manages and distributes scientific data, creates tools for data access, supports data users, performs scientific research, and educates the public about the cryosphere. 4 | CSAS;Center For Snow & Avalanche Studies;https://snowstudies.org/;The Center for Snow & Avalanche Studies and our Senator Beck Basin Study Area serve the mountain science community and regional resource managers by hosting and conducting interdisciplinary research and sustaining integrative 24/7/365 monitoring that captures weather, snowpack, radiation, soils, plant communities and hydrologic signals of regional climate trends. Senator Beck Basin is also the home of the Colorado Dust-on-Snow Program. The slide show above illustrates some of the research teams we've hosted, and we invite additional researchers to utilize Senator Beck Basin facilities and data. 5 | Sierra Snow Pack;CA Data Exchange Center;https://cdec.water.ca.gov/snow/current/snow/;The California Data Exchange Center (CDEC) installs, maintains, and operates an extensive hydrologic data collection network including automatic snow reporting gages for the Cooperative Snow Surveys Program and precipitation and river stage sensors for flood forecasting. 6 | Central Sierra Snow Lab;UC Berkeley research field station;http://vcresearch.berkeley.edu/research-unit/central-sierra-snow-lab;Located at Donner Pass in the Sierra Nevada, the Central Sierra Snow Laboratory is a research field station of UC Berkeley specializing in snow physics, snow hydrology, meteorology, climatology, and instrument design. Built in 1946 by the (then) U.S. Weather Bureau and Army Corps of Engineers, it is administered by UC Berkeley's Vice Chancellor for Research. 7 | NASA;Hydrological Sciences - Science Research Portal;https://neptune.gsfc.nasa.gov/hsb/index.php?section=369;Changes in snow quantity and snowmelt timing are underway and have serious consequences. However, the quantity of snow stored across the prairies and tundra, in the mountains, on sea ice, and in the forests remains difficult to measure, hindering efforts to understand how, why and where this precious resource is changing. Airborne and satellite remote sensing provides a means of measuring these changes in a consistent manner over large areas on a regular basis, but developing the optimal instruments to measure snow requires an investment of dollars and a long-term (several decades) effort. NASA seeks to measure global snow cover using multiple approaches including aircraft and field campaigns as well as through modeling and data assimilation, all derived from competitively-selected research activities. 8 | Snowmelt Modeling;CU snowmelt modeling MS thesis;http://libraries.colorado.edu/record=b2684630~S3;University of Colorado library call# T 1996 .M724 c.2 - Monitoring and modeling of snowmelt at Rocky Flats / by James Andrew Moffitt -- Snowmelt modeling is based on energy and mass balances and the identification and quantification of the energy fluxes that influence snowmelt. -------------------------------------------------------------------------------- /app/helpers/get_resources.rb: -------------------------------------------------------------------------------- 1 | #Attempting to abstract away all the 'resource' metadata and management into this class. 2 | #This class knows where things are stored (on Heroku at least) 3 | #Could have 'dev helper' features for working on different platforms (heroku, local linux, ?). 4 | #Sets up all object variables needed by Bot. One Stop Shop. 5 | 6 | #Key feature design details: 7 | 8 | # This bot builds Twitter Quick Reply (menu) lists from a set of local data files. These files are served from the webhook listener server... 9 | # Bot supports: 10 | # * Single list of locations (up to 20) 11 | # * Snowbot uses snow resort locations, @FloodSocial uses Texas cities. 12 | # * Look-up for a single list of curated links. 13 | # * Snowbot presents a short of list of suggested 'learn more' URLs. 14 | # * Serves photos hosted in single directory of JPEGs. 15 | # * Snowbot displays random snow photos. 16 | # * Single list of playlist URLs. Geo-tagged music, why not? 17 | 18 | 19 | class GetResources 20 | require 'csv' 21 | 22 | attr_accessor :photos_home, 23 | :photos_list, #CSV with file name and caption. That's it. 24 | 25 | :locations_home, 26 | :locations_list, #This class knows the configurable location list. 27 | 28 | :links_home, 29 | :links_list, 30 | 31 | :playlists_home, 32 | :playlists_list 33 | 34 | def initialize() 35 | 36 | #Load resources, populating attributes. 37 | @photos_home = 'app/config/data/photos' #On Heroku at least. 38 | if not File.directory?(@photos_home) 39 | @photos_home = '../config/data/photos' 40 | end 41 | @photos_list = [] 42 | @photos_list = get_photos 43 | 44 | @links_home = 'app/config/data/links' #On Heroku at least. 45 | if not File.directory?(@links_home) 46 | @links_home = '../config/data/links' 47 | end 48 | @links_list = [] 49 | @links_list = get_links 50 | 51 | @locations_home = 'app/config/data/locations' #On Heroku at least. 52 | if not File.directory?(@locations_home) 53 | @locations_home = '../config/data/locations' 54 | end 55 | @locations_list = [] 56 | @locations_list = get_locations 57 | 58 | @playlists_home = 'app/config/data/music' #On Heroku at least. 59 | if not File.directory?(@playlists_home) 60 | @playlists_home = '../config/data/music' 61 | end 62 | @playlists_list = [] 63 | @playlists_list = get_playlists 64 | end 65 | 66 | #Take resource file with '#' comment lines and filter them out. 67 | #Filter out '#' comment lines. 68 | def filter_list(lines) 69 | list = [] 70 | 71 | lines.each do |line| 72 | if line[0][0] != '#' 73 | #drop dynamically from array 74 | list << line 75 | end 76 | end 77 | list 78 | end 79 | 80 | #photo_list = [] #Load array of photo metadata. 81 | def get_photos 82 | list = [] 83 | 84 | begin 85 | list = filter_list(CSV.read("#{@photos_home}/photos.csv", {:col_sep => ";"})) 86 | #puts "Have a list of #{photo_list.count} photos..." 87 | rescue 88 | end 89 | 90 | list 91 | end 92 | 93 | #list = [] #Load array of curated links. 94 | def get_links 95 | list = [] 96 | 97 | begin 98 | list = filter_list(CSV.read("#{@links_home}/links.csv", {:col_sep => ";"})) 99 | #puts "Have a list of #{list.count} links..." 100 | rescue 101 | end 102 | 103 | list 104 | end 105 | 106 | #list = [] #Load array of curated locations. 107 | def get_locations 108 | list = [] 109 | 110 | begin 111 | list = filter_list(CSV.read("#{@locations_home}/placesOfInterest.csv")) 112 | rescue 113 | end 114 | 115 | #puts "Have a list of #{list.count} locations..." 116 | list 117 | end 118 | 119 | #list = [] #Load array of curated locations. 120 | def get_playlists 121 | list = [] 122 | 123 | begin 124 | list = filter_list(CSV.read("#{@playlists_home}/playlists.csv")) 125 | #puts "Have a list of #{list.count} playlists..." 126 | rescue 127 | end 128 | 129 | list 130 | end 131 | 132 | #======================= 133 | if __FILE__ == $0 #This script code is executed when running this file. 134 | retriever = GetResources.new 135 | 136 | #Example code for loading location file -------- 137 | retriever.locations_home = '/Users/jmoffitt/work/snowbotdev/data/locations' 138 | locations = retriever.get_locations 139 | 140 | locations.each do |resorts| #explore that list 141 | puts resorts 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /app/helpers/third_party_request.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'open-uri' 3 | 4 | class ThirdPartyRequest 5 | 6 | HEADERS = {"content-type" => "application/json"} #Suggested set? Any? 7 | 8 | attr_accessor :keys, 9 | :base_url, #Default: 'https://api.twitter.com/' 10 | :uri_path #No default. 11 | 12 | def initialize(config_file = nil) 13 | 14 | #Get Twitter App keys and tokens. Pull config details from ENV{} hash. 15 | @keys = {} 16 | 17 | @keys['weather_consumer_key'] = ENV['WEATHERUNDERGROUND_KEY'] 18 | @keys['snocountry_consumer_key'] = ENV['SNOCOUNTRY_KEY'] 19 | 20 | end 21 | 22 | #This is not used in Snow Bot yet. 23 | #http://feeds.snocountry.net/conditions.php?apiKey=KEY_ID&resortType=Alpine&action=top20 24 | def get_top_snow_resorts 25 | 26 | open("http://feeds.snocountry.net/conditions.php?apiKey=#{@keys['snocountry_consumer_key']}&resortType=Alpine&action=top20") do |f| 27 | json_string = f.read 28 | 29 | parsed_json = JSON.parse(json_string) 30 | 31 | return parsed_json 32 | 33 | end 34 | 35 | 36 | end 37 | 38 | 39 | #http://feeds.snocountry.net/conditions.php?apiKey=KEY_ID&ids=303001 40 | def get_resort_info(resort_id) 41 | 42 | open("http://feeds.snocountry.net/conditions.php?apiKey=#{@keys['snocountry_consumer_key']}&ids=#{resort_id}") do |f| 43 | json_string = f.read 44 | parsed_json = JSON.parse(json_string) 45 | 46 | #if parsed_json['openDownHillLifts'] && parsed_json['openDownHillTrails'] 47 | # return "Ski area is not open." 48 | #end 49 | 50 | resort_data = parsed_json["items"][0] 51 | 52 | #"operatingStatus" --> "Open for Events\/Activities", "" 53 | 54 | name = resort_data["resortName"] 55 | last_snow_date = resort_data["lastSnowFallDate"] 56 | last_snow_amount = resort_data["lastSnowFallAmount"] 57 | prev_snow_date = resort_data["prevSnowFallDate"] 58 | prev_snow_amount = resort_data["prevSnowFallAmount"] 59 | open_lifts = resort_data["openDownHillLifts"] 60 | total_lifts = resort_data["maxOpenDownHillLifts"] 61 | open_trails = resort_data["openDownHillTrails"] 62 | total_trails = resort_data["maxOpenDownHillTrails"] 63 | open_acres = resort_data["openDownHillAcres"] 64 | total_acres = resort_data["maxOpenDownHillAcres"] 65 | web_site_link = resort_data["webSiteLink"] 66 | trail_map = resort_data["lgTrailMapURL"] 67 | 68 | resort_summary = "#{name} report:\n" + 69 | "* Last snow: #{last_snow_amount} inches on #{last_snow_date}\n" + 70 | "* Previous snow: #{prev_snow_amount} inches on #{prev_snow_date}\n" + 71 | "* Open lifts: #{open_lifts} / #{total_lifts}\n" + 72 | "* Open trails: #{open_trails} / #{total_trails}\n" + 73 | "* Open acres: #{open_acres} / #{total_acres}\n" + 74 | "* Web site: #{web_site_link}\n" + 75 | "* Trail map: #{trail_map}\n" 76 | 77 | 78 | return resort_summary 79 | 80 | end 81 | end 82 | 83 | #http://api.wunderground.com/api/APIKEY/forecast/astronomy/conditions/q/42.077201843262,-8.4819002151489.json 84 | def get_current_weather(lat,long) 85 | 86 | open("http://api.wunderground.com/api/#{@keys['weather_consumer_key']}/geolookup/conditions/q/#{lat},#{long}.json") do |f| 87 | json_string = f.read 88 | parsed_json = JSON.parse(json_string) 89 | 90 | if parsed_json['response'] && parsed_json['response']['error'] 91 | return "No weather report available for that location" 92 | end 93 | 94 | #Generate place name 95 | city = parsed_json['location']['city'] 96 | state = parsed_json['location']['state'] 97 | country = parsed_json['location']['country_name'] 98 | place_name = "#{city}, #{state}, #{country}" 99 | 100 | #Generate weather summary 101 | weather = parsed_json['current_observation']['weather'] 102 | temp = parsed_json['current_observation']['temperature_string'] 103 | feels_like = parsed_json['current_observation']['feelslike_string'] 104 | wind = parsed_json['current_observation']['wind_string'] 105 | rain_today = parsed_json['current_observation']['precip_today_string'] 106 | forecast_url = parsed_json['current_observation']['forecast_url'] 107 | 108 | weather_summary = "* #{weather}\n" + 109 | "* Current temp: #{temp}\n" + 110 | "* Feels like: #{feels_like} \n" + 111 | "* Wind speed: #{wind} \n" + 112 | "* Rain today: #{rain_today} \n" + 113 | "--> #{forecast_url}\n" 114 | 115 | return "Current weather conditions in #{place_name}: \n #{weather_summary}" 116 | end 117 | 118 | end 119 | 120 | end 121 | 122 | if __FILE__ == $0 #This script code is executed when running this file. 123 | 124 | thirdPartyAPI = ThirdPartyRequest.new 125 | 126 | #Testing WeatherUnderground 127 | response = thirdPartyAPI.get_current_weather(40.0150,-105.2705) 128 | puts response 129 | 130 | #Given Resort name, look up resort ID at www.SnoCountry.com 131 | response = thirdPartyAPI.get_resort_info(303001) 132 | puts response 133 | 134 | end -------------------------------------------------------------------------------- /app/helpers/event_manager.rb: -------------------------------------------------------------------------------- 1 | #POST requests to /webhooks/twitter arrive here. 2 | #Twitter Account Activity API send events as POST requests with DM JSON payloads. 3 | 4 | require 'json' 5 | #Two helper classes... 6 | require_relative 'send_direct_message' 7 | 8 | class EventManager 9 | #Design: Identifying explicit commands is easy and can restrict text length based on its own length. 10 | # If you want to be more flexible, 11 | COMMAND_MESSAGE_LIMIT = 12 #Simplistic way to detect an incoming, short, 'commmand' DM. 12 | 13 | attr_accessor :DMsender 14 | 15 | def initialize 16 | #puts 'Creating EventManager object' 17 | @DMSender = SendDirectMessage.new 18 | end 19 | 20 | def handle_quick_reply(dm_event) 21 | 22 | response_metadata = dm_event['message_create']['message_data']['quick_reply_response']['metadata'] 23 | user_id = dm_event['message_create']['sender_id'] 24 | 25 | #Default options 26 | if response_metadata == 'help' 27 | @DMSender.send_system_help(user_id) 28 | 29 | elsif response_metadata == 'learn_more' 30 | @DMSender.send_system_info(user_id) 31 | 32 | elsif response_metadata == 'return_home' 33 | puts "Returning to home in event manager...." 34 | @DMSender.send_welcome_message(user_id) 35 | 36 | #Custom options 37 | elsif response_metadata == 'see_photo' 38 | @DMSender.send_photo(user_id) 39 | 40 | elsif response_metadata == 'learn_snow' 41 | @DMSender.send_links_list(user_id) 42 | 43 | elsif response_metadata == 'snow_music' 44 | @DMSender.send_playlists_list(user_id) 45 | 46 | elsif response_metadata.include? 'link_choice' 47 | choice = response_metadata['link_choice: '.length..-1] 48 | @DMSender.send_link(user_id, choice) 49 | 50 | elsif response_metadata.include? 'playlist_choice' 51 | choice = response_metadata['playlist_choice: '.length..-1] 52 | @DMSender.send_playlist(user_id, choice) 53 | 54 | elsif response_metadata == 'snow_report' 55 | @DMSender.send_locations_list(user_id) 56 | 57 | elsif response_metadata.include? 'location_choice' 58 | choice = response_metadata['location_choice: '.length..-1] 59 | @DMSender.send_location_info(user_id, choice) 60 | 61 | elsif response_metadata.include? 'go_back' 62 | type = response_metadata['go_back'.length..-1].strip 63 | 64 | if type == 'links' 65 | @DMSender.send_links_list(user_id) 66 | elsif type == 'locations' 67 | @DMSender.send_locations_list(user_id) 68 | elsif type == 'playlists' 69 | @DMSender.send_playlists_list(user_id) 70 | end 71 | 72 | elsif response_metadata.include? 'location_choice' 73 | 74 | location_choice = response_metadata['location_choice: '.length..-1] 75 | 76 | #Get coordinates 77 | coordinates = [] 78 | 79 | location_choice = "#{location_choice} (centered at #{coordinates[0]}, #{coordinates[1]} to be specific)" 80 | @DMSender.respond_to_location_choice(user_id, location_choice) 81 | 82 | else #we have an answer to one of the above. 83 | puts "UNHANDLED user response: #{response_metadata}" 84 | end 85 | 86 | end 87 | 88 | def handle_command(dm_event) 89 | 90 | #Since this DM is not a response to a QR, let's check for other 'action' commands 91 | 92 | request = dm_event['message_create']['message_data']['text'] 93 | user_id = dm_event['message_create']['sender_id'] 94 | 95 | if request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'bot' or request.downcase.include? 'home' or request.downcase.include? 'main' or request.downcase.include? 'hello' or request.downcase.include? 'back') 96 | @DMSender.send_welcome_message(user_id) 97 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'photo' or request.downcase.include? 'pic' or request.downcase.include? 'see') 98 | @DMSender.send_photo(user_id) 99 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'wx' or request.downcase.include? 'weather') 100 | @DMSender.send_map(user_id) 101 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'report' or request.downcase.include? 'resort' or request.downcase.include? 'rpt') 102 | @DMSender.send_locations_list(user_id) 103 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'learn') 104 | @DMSender.send_links_list(user_id) 105 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'playlist' or request.downcase.include? 'music') 106 | @DMSender.send_playlists_list(user_id) 107 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'about') 108 | @DMSender.send_system_info(user_id) 109 | elsif request.length <= COMMAND_MESSAGE_LIMIT and (request.downcase.include? 'help') 110 | @DMSender.send_system_help(user_id) 111 | else 112 | # This is where you'd plug in more fancy message processing... 113 | #message = "I only support a basic set of commands, send 'help' to review those... " 114 | #@DMSender.send_custom_message(user_id, message) 115 | end 116 | end 117 | 118 | #responses are based on options' Quick Reply metadata settings. 119 | #pick_from_list, select_on_map, location list items (e.g. 'location_list_choice: Austin' or 'Fort Worth') 120 | #map_selection (triggers a fetch of the shared coordinates) 121 | 122 | def handle_event(events) 123 | 124 | events = JSON.parse(events) 125 | 126 | if events.key? ('direct_message_events') 127 | 128 | dm_events = events['direct_message_events'] 129 | 130 | dm_events.each do |dm_event| 131 | 132 | if dm_event['type'] == 'message_create' 133 | 134 | #Is this a response? Test for the 'quick_reply_response' key. 135 | is_response = dm_event['message_create'] && dm_event['message_create']['message_data'] && dm_event['message_create']['message_data']['quick_reply_response'] 136 | 137 | if is_response 138 | handle_quick_reply dm_event 139 | else 140 | handle_command dm_event 141 | end 142 | else 143 | puts "A unhandled DM type arrived via Twitter Account Activity API... " 144 | end 145 | end 146 | else 147 | puts "Hey, a unhandled Account Activity event has been send from the Twitter side... A new follower? A Tweet liked? " 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /scripts/setup_welcome_messages.rb: -------------------------------------------------------------------------------- 1 | #Code for managing Default Welcome Messages. 2 | #Note that this code currently does not have support for running on Heroku. To add that the configuration details 3 | #(OAuth keys) should be loaded from the ENV[] structure. 4 | 5 | require 'json' 6 | require 'optparse' 7 | 8 | require_relative '../app/helpers/api_oauth_request' 9 | require_relative '../app/helpers/generate_direct_message_content' 10 | 11 | class WelcomeMessageManager 12 | 13 | attr_accessor :twitter_api, 14 | :message_generator, 15 | :uri_path 16 | 17 | 18 | def initialize() 19 | 20 | @twitter_api = ApiOauthRequest.new 21 | @uri_path = '/1.1/direct_messages' 22 | @twitter_api.get_api_access 23 | 24 | @message_generator = GenerateDirectMessageContent.new(true) 25 | 26 | end 27 | 28 | def create_welcome_message(message) 29 | puts "Creating Welcome Message..." 30 | 31 | uri_path = "#{@uri_path}/welcome_messages/new.json" 32 | 33 | response = @twitter_api.make_post_request(uri_path, message) 34 | results = JSON.parse(response) 35 | 36 | if not results['errors'].nil? 37 | puts "Errors occurred." 38 | errors = [] 39 | errors = results['errors'] 40 | errors.each do |error| 41 | puts error 42 | end 43 | 44 | else 45 | results = JSON.parse(response) 46 | puts results 47 | end 48 | end 49 | 50 | def create_maintenance_message 51 | welcome_message = @message_generator.generate_system_maintenance_welcome 52 | welcome_message 53 | end 54 | 55 | def create_default_welcome_message 56 | welcome_message = @message_generator.generate_welcome_message_default 57 | welcome_message 58 | end 59 | 60 | 61 | def set_default_welcome_message(message_id) 62 | 63 | puts "Setting default Welcome Message to message with id #{message_id}..." 64 | 65 | set_rule = {} 66 | set_rule['welcome_message_rule'] = {} 67 | set_rule['welcome_message_rule']['welcome_message_id'] = message_id 68 | 69 | uri_path = "#{@uri_path}/welcome_messages/rules/new.json" 70 | 71 | response = @twitter_api.make_post_request(uri_path, set_rule.to_json) 72 | results = JSON.parse(response) 73 | 74 | rule_id = results['id'] 75 | puts rule_id 76 | 77 | end 78 | 79 | def get_welcome_messages 80 | 81 | puts "Getting welcome message list." 82 | 83 | uri_path = "#{@uri_path}/welcome_messages/list.json" 84 | response = @twitter_api.make_get_request(uri_path) 85 | 86 | if response == '{}' 87 | puts "No Welcome Messages created." 88 | results = nil 89 | else 90 | results = JSON.parse(response) 91 | end 92 | 93 | results 94 | 95 | end 96 | 97 | def delete_welcome_message(id) 98 | 99 | puts "Deleting Welcome Message with id: #{id}." 100 | 101 | uri_path = "#{@uri_path}/welcome_messages/destroy.json?id=#{id}" 102 | response = @twitter_api.make_delete_request(uri_path) 103 | 104 | if response == '204' 105 | puts "Deleted message id: #{id} (if it existed)." 106 | else 107 | puts "Failed to delete message id: #{id}" 108 | end 109 | end 110 | 111 | def delete_all_welcome_messages(messages) 112 | 113 | messages.each do |message| 114 | delete_welcome_message(message["id"]) 115 | end 116 | 117 | end 118 | 119 | # Rules ------------------------------------------------------ 120 | 121 | def get_message_rules 122 | 123 | puts "Getting welcome message rules list." 124 | 125 | uri_path = "#{@uri_path}/welcome_messages/rules/list.json" 126 | response = @twitter_api.make_get_request(uri_path) 127 | 128 | if response == '{}' 129 | puts "No rules exist." 130 | else 131 | results = JSON.parse(response) 132 | results 133 | end 134 | end 135 | 136 | def create_message_rule(id) 137 | 138 | puts "Setting message rule for id #{id}..." 139 | 140 | set_rule = {} 141 | set_rule['welcome_message_rule'] = {} 142 | set_rule['welcome_message_rule']['welcome_message_id'] = id 143 | 144 | uri_path = "#{@uri_path}/welcome_messages/rules/new.json" 145 | 146 | response = @twitter_api.make_post_request(uri_path, set_rule.to_json) 147 | results = JSON.parse(response) 148 | 149 | rule_id = results['id'] 150 | puts rule_id 151 | 152 | end 153 | 154 | 155 | def delete_message_rule(id) 156 | 157 | puts "Deleting rule with id: #{id}." 158 | 159 | uri_path = "#{@uri_path}/welcome_messages/rules/destroy.json?id=#{id}" 160 | response = @twitter_api.make_delete_request(uri_path) 161 | 162 | results = JSON.parse(response) 163 | 164 | results 165 | 166 | end 167 | 168 | end 169 | 170 | 171 | #======================================================================================================================= 172 | if __FILE__ == $0 #This script code is executed when running this file. 173 | 174 | #Supporting any command-line options? Handle here. 175 | #options: -config -id -url 176 | OptionParser.new do |o| 177 | 178 | #Passing in the task at hand. Default Welcome Message management or Welcome Message rule management. 179 | o.on('-w WELCOME', '--default', "Default Welcome Management: 'create', 'set', 'get', 'delete'") { |welcome| $welcome = welcome } 180 | o.on('-r RULE', '--rule', "Welcome Message Rule management: 'create', 'get', 'delete'") { |rule| $rule = rule } 181 | o.on('-i ID', '--id', 'Message or rule ID') { |id| $id = id } 182 | 183 | #Help screen. 184 | o.on('-h', '--help', 'Display this screen.') do 185 | puts o 186 | exit 187 | end 188 | 189 | o.parse! 190 | end 191 | 192 | message_manager = WelcomeMessageManager.new 193 | 194 | if $welcome == 'create' #Ad hoc, not too often? 195 | message_manager.create_welcome_message(message_manager.create_default_welcome_message) 196 | #message_manager.create_welcome_message(message_manager.create_maintenance_message) 197 | 198 | elsif $welcome == 'set' 199 | 200 | message_manager.set_default_welcome_message($id) 201 | 202 | elsif $welcome == 'get' 203 | welcome_messages = message_manager.get_welcome_messages 204 | 205 | if not welcome_messages.nil? 206 | 207 | messages = welcome_messages["welcome_messages"] 208 | 209 | puts "Message IDs: " 210 | 211 | messages.each do |message| 212 | puts "Message ID #{message["id"]} with message: #{message["message_data"]["text"]} " 213 | end 214 | 215 | end 216 | 217 | elsif $welcome == 'delete' 218 | message_manager.delete_welcome_message($id) 219 | 220 | elsif $welcome == 'delete_all' 221 | welcome_messages = message_manager.get_welcome_messages 222 | 223 | messages = welcome_messages["welcome_messages"] 224 | 225 | message_manager.delete_all_welcome_messages(messages) 226 | 227 | elsif $rule == 'create' 228 | message_manager.create_message_rule($id) 229 | elsif $rule == 'get' 230 | rules = message_manager.get_message_rules 231 | 232 | rules["welcome_message_rules"].each do |rule| 233 | puts "Rule #{rule["id"]} points to #{rule["welcome_message_id"]}" 234 | end 235 | #864475517260636161 236 | 237 | elsif $rule == 'delete' 238 | message_manager.delete_message_rule($id) 239 | end 240 | 241 | end 242 | -------------------------------------------------------------------------------- /scripts/setup_webhooks.rb: -------------------------------------------------------------------------------- 1 | #Code for configuring webhook details for the Account Activity API. 2 | #Note that this code currently does not have support for running on Heroku. To add that the configuration details 3 | #(OAuth keys) should be loaded from the ENV[] structure. 4 | 5 | require 'json' 6 | require 'cgi' 7 | require 'optparse' 8 | 9 | require_relative '../app/helpers/api_oauth_request' 10 | 11 | class TaskManager 12 | 13 | attr_accessor :twitter_api, 14 | :api_tier, #Introduction of Premium AA API, neccessitates new config metadata. 15 | :env_name, 16 | :webhook_configs, #An array of Webhook IDs. Currently numeric(e.g. 88888888888888), subject to change? 17 | :uri_path #This gets built, depending on the API tier, and is passed to the api_oauth_request manager. 18 | 19 | def initialize(config=nil) 20 | 21 | @uri_path = "/1.1/account_activity" 22 | 23 | #Create a 'wrapper' to the the Twitter Account Activity API, and get authenticated. 24 | @twitter_api = ApiOauthRequest.new(config) 25 | #OAuth keys are loaded from ENV by ApiOAuthRequest object, unless a config file is specified. 26 | #@keys['consumer_key'] = ENV['CONSUMER_KEY'] 27 | #@keys['consumer_secret'] = ENV['CONSUMER_SECRET'] 28 | #@keys['access_token'] = ENV['ACCESS_TOKEN'] 29 | #@keys['access_token_secret'] = ENV['ACCESS_TOKEN_SECRET'] 30 | 31 | @twitter_api.get_api_access 32 | 33 | @webhook_configs = [] 34 | 35 | end 36 | 37 | def get_webhook_configs 38 | puts "Retrieving webhook configurations..." 39 | 40 | @twitter_api.get_api_access 41 | 42 | if @api_tier == 'premium' 43 | @uri_path = "#{@uri_path}/all/#{@env_name}/webhooks.json" 44 | else 45 | @uri_path = "#{@uri_path}/webhooks.json" 46 | end 47 | 48 | response = @twitter_api.make_get_request(@uri_path) 49 | 50 | results = JSON.parse(response) 51 | 52 | if results.count == 0 53 | puts "No existing configurations... " 54 | else 55 | results.each do |result| 56 | @webhook_configs << result 57 | #TODO: puts "Webhook ID #{result['id']} --> #{result['url']}" 58 | end 59 | end 60 | 61 | @webhook_configs 62 | end 63 | 64 | def set_webhook_config(url) 65 | puts "Setting a webhook configuration..." 66 | 67 | if @api_tier == 'premium' 68 | @uri_path = "#{@uri_path}/all/#{@env_name}/webhooks.json?url=#{url}" 69 | else 70 | @uri_path = "#{@uri_path}/webhooks.json?url=#{url}" 71 | end 72 | 73 | response = @twitter_api.make_post_request(@uri_path,nil) 74 | results = JSON.parse(response) 75 | 76 | if results['errors'].nil? 77 | puts "Created webhook instance with webhook_id: #{results['id']} | pointing to #{results['url']}" 78 | else 79 | puts results['errors'] 80 | end 81 | 82 | results 83 | end 84 | 85 | # id: webhook_id (enterprise), :env_name (premium) 86 | def delete_webhook_config(id, name=nil) 87 | puts "Attempting to delete configuration for webhook id: #{id}." 88 | 89 | if @api_tier == 'premium' 90 | @uri_path = "#{@uri_path}/all/#{name}/webhooks/#{id}.json" 91 | else 92 | @uri_path = "#{@uri_path}/webhooks/#{id}.json" 93 | end 94 | 95 | response = @twitter_api.make_delete_request(@uri_path) 96 | 97 | if response == '204' 98 | puts "Webhook configuration for #{id} was successfully deleted." 99 | else 100 | puts response 101 | end 102 | 103 | response 104 | end 105 | 106 | # id: webhook_id (enterprise), :env_name (premium) 107 | def get_webhook_subscription(id) 108 | puts "Retrieving webhook subscriptions..." 109 | 110 | if @api_tier == 'premium' 111 | @uri_path = "#{@uri_path}/all/#{id}/subscriptions.json" 112 | else 113 | @uri_path = "#{@uri_path}/webhooks/#{id}/subscriptions/all.json" 114 | end 115 | 116 | response = @twitter_api.make_get_request(@uri_path) 117 | 118 | if response == '204' 119 | puts "Webhook subscription exists for #{id}." 120 | else 121 | puts "Webhook subscription does not exist for #{id}." 122 | end 123 | 124 | response 125 | end 126 | 127 | # Sets a subscription for current User context. 128 | # # id: webhook_id (enterprise), :env_name (premium) 129 | def set_webhook_subscription(id) 130 | puts "Setting subscription for 'host' account for webhook id: #{id}" 131 | 132 | if @api_tier == 'premium' 133 | @uri_path = "#{@uri_path}/all/#{id}/subscriptions.json" 134 | else 135 | @uri_path = "#{@uri_path}/webhooks/#{id}/subscriptions/all.json" 136 | end 137 | 138 | response = @twitter_api.make_post_request(@uri_path, nil) 139 | 140 | if response == '204' 141 | puts "Webhook subscription for #{id} was successfully added." 142 | else 143 | puts response 144 | end 145 | 146 | response 147 | end 148 | 149 | # id: webhook_id (enterprise), :env_name (premium) 150 | def delete_webhook_subscription(id) 151 | puts "Attempting to delete subscription for webhook: #{id}." 152 | 153 | if @api_tier == 'premium' 154 | @uri_path = "#{@uri_path}/all/#{id}/subscriptions/all.json" 155 | else 156 | @uri_path = "#{@uri_path}/webhooks/#{id}/subscriptions.json" 157 | end 158 | 159 | response = @twitter_api.make_delete_request(@uri_path) 160 | 161 | if response == '204' 162 | puts "Webhook subscription for #{id} was successfully deleted." 163 | else 164 | puts response 165 | end 166 | 167 | response 168 | end 169 | 170 | 171 | def confirm_crc(id) 172 | 173 | if @api_tier == 'premium' 174 | @uri_path = "#{@uri_path}/all/#{@env_name}/webhooks/#{id}.json" 175 | else 176 | @uri_path = "#{@uri_path}/webhooks/#{id}.json" 177 | end 178 | 179 | response = @twitter_api.make_put_request(uri_path) 180 | 181 | puts response 182 | 183 | if response == '204' 184 | puts "CRC request successful and webhook status set to valid." 185 | else 186 | puts "Webhook URL does not meet the requirements. Please consult: https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/guides/securing-webhooks" 187 | end 188 | 189 | response 190 | 191 | end 192 | 193 | end 194 | 195 | #======================================================================================================================= 196 | 197 | #trigger CRC ('crc'), 198 | #set config ('set'), 199 | #list configs ('list'), 200 | #delete config ('delete'), 201 | #subscribe app ('subscribe'), 202 | #unsubscribe app ('unsubscribe') 203 | #get subscription ('subscription').") 204 | 205 | if __FILE__ == $0 #This script code is executed when running this file. 206 | 207 | #Supporting any command-line options? Handle here. 208 | #options: -config -id -url 209 | OptionParser.new do |o| 210 | 211 | #Passing in a config file.... Or you can set a bunch of parameters. 212 | o.on('-c CONFIG', '--config', 'If not setting ENV variableds, you can specify a configuration file (including path) that provides OAuth details. ') { |config| $config = config} 213 | o.on('-t TASK','--task', "Securing Webhooks Task to perform: trigger CRC ('crc'), set config ('set'), list configs ('list'), delete config ('delete'), subscribe app ('subscribe'), unsubscribe app ('unsubscribe'),get subscription ('subscription').") {|task| $task = task} 214 | o.on('-u URL','--url', "Webhooks 'consumer' URL, e.g. https://mydomain.com/webhooks/twitter.") {|url| $url = url} 215 | o.on('-i ID','--id', 'Webhook ID') {|id| $id = id} 216 | o.on('-n NAME', '--name', 'Premium environment name. Required with the premium tier.'){|name| $name = name} 217 | 218 | #Help screen. 219 | o.on( '-h', '--help', 'Display this screen.' ) do 220 | puts o 221 | exit 222 | end 223 | 224 | o.parse! 225 | end 226 | 227 | if $task.nil? then 228 | $task = 'list' 229 | end 230 | 231 | task_manager = TaskManager.new($config) 232 | 233 | #If a name is provided, then we are working with Premium tier. If not, then Enterprise. 234 | if $name.nil? then 235 | task_manager.api_tier = 'enterprise' 236 | else 237 | task_manager.api_tier = 'premium' 238 | task_manager.env_name = $name 239 | end 240 | 241 | if task_manager.api_tier == 'premium' and $task != 'crc' 242 | $id = task_manager.env_name 243 | end 244 | 245 | if $task == 'list' 246 | configs = task_manager.get_webhook_configs 247 | configs.each do |config| 248 | puts "Webhook ID #{config['id']} --> #{config['url']}" 249 | end 250 | elsif $task == 'set' 251 | if $url.nil? 252 | puts "Must provide a URL when establishing a webhook. Quitting. " 253 | exit 254 | else 255 | url = CGI::escape($url) 256 | end 257 | 258 | task_manager.set_webhook_config(url) 259 | elsif $task == 'delete' 260 | task_manager.delete_webhook_config($id) 261 | elsif $task == 'subscribe' 262 | task_manager.set_webhook_subscription($id) 263 | elsif $task == 'unsubscribe' 264 | task_manager.delete_webhook_subscription($id) 265 | elsif $task == 'subscription' 266 | task_manager.get_webhook_subscription($id) 267 | elsif $task == 'crc' #triggers a CRC for all configured webhook ids unless a specific id is passed in. 268 | 269 | if $id.nil? 270 | puts "Triggering a CRC requires a webhook ID to be passed. You can run the 'list' task to retrive those." 271 | else 272 | result = task_manager.confirm_crc($id) 273 | puts result 274 | end 275 | 276 | else 277 | puts "Unhandled task. Available tasks: 'list', 'crc', 'set', 'subscribe', 'delete'" 278 | end 279 | end 280 | -------------------------------------------------------------------------------- /app/helpers/generate_direct_message_content.rb: -------------------------------------------------------------------------------- 1 | #Generates all content for bot Direct Messages. 2 | #Many responses include configuration metadata/resources, such as photos, links, and location list. 3 | #These metadata are loading from local files. 4 | #Direct Messages with media require a side call to Twitter upload endpoint, so this class uses a Twitter API object. 5 | 6 | require_relative 'twitter_api' #Hooks to Twitter Public APIs via 'twitter' gem. 7 | require_relative 'third_party_request' #Hooks to third-party APIs. 8 | require_relative 'get_resources' #Loads local resources used to present DM menu options and photos to users. 9 | 10 | class GenerateDirectMessageContent 11 | 12 | VERSION = 0.888 13 | BOT_NAME = 'SnowBotDev' 14 | BOT_CHAR = '❄' 15 | 16 | attr_accessor :TwitterAPI, 17 | :resources, 18 | :thirdparty_api 19 | 20 | def initialize(setup=nil) #'Setup Welcome Message' script using this too, but does not require many helper objects. 21 | 22 | #puts "Creating GenerateDirectMessageContent object." 23 | 24 | if setup.nil? 25 | @twitter_api = TwitterAPI.new 26 | @resources = GetResources.new 27 | @thirdparty_api = ThirdPartyRequest.new 28 | end 29 | 30 | end 31 | 32 | #================================================================ 33 | def generate_random_photo(recipient_id) 34 | 35 | #Build DM content. 36 | event = {} 37 | event['event'] = message_create_header(recipient_id) 38 | 39 | message_data = {} 40 | 41 | #Select photo(at random). 42 | photo = @resources.photos_list.sample 43 | message = photo[1] 44 | message_data['text'] = message 45 | 46 | #Confirm photo file exists 47 | photo_file = "#{@resources.photos_home}/#{photo[0]}" 48 | 49 | if File.file? photo_file 50 | media_id = @twitter_api.get_media_id(photo_file) 51 | 52 | attachment = {} 53 | attachment['type'] = "media" 54 | attachment['media'] = {} 55 | attachment['media']['id'] = media_id 56 | 57 | message_data['attachment'] = attachment 58 | 59 | else 60 | media_id = nil 61 | message = "Sorry, could not load photo: #{photo_file}." 62 | end 63 | 64 | message_data['quick_reply'] = {} 65 | message_data['quick_reply']['type'] = 'options' 66 | 67 | options = [] 68 | options = build_photo_option 69 | options += build_home_option('with_description') 70 | 71 | message_data['quick_reply']['options'] = options 72 | 73 | event['event']['message_create']['message_data'] = message_data 74 | 75 | event.to_json 76 | 77 | end 78 | 79 | def generate_playlist_list(recipient_id) 80 | 81 | event = {} 82 | event['event'] = message_create_header(recipient_id) 83 | 84 | message_data = {} 85 | message_data['text'] = 'Select a playlist:' 86 | 87 | message_data['quick_reply'] = {} 88 | message_data['quick_reply']['type'] = 'options' 89 | 90 | options = [] 91 | 92 | @resources.playlists_list.each do |item| 93 | if item.count > 0 94 | option = {} 95 | option['label'] = "#{BOT_CHAR} " + item[0] 96 | option['metadata'] = "playlist_choice: #{item[0]}" 97 | option['description'] = item[1] 98 | options << option 99 | end 100 | end 101 | 102 | options += build_home_option('with description') 103 | 104 | message_data['quick_reply']['options'] = options 105 | 106 | event['event']['message_create']['message_data'] = message_data 107 | event.to_json 108 | 109 | end 110 | 111 | def generate_playlist(recipient_id, playlist_choice) 112 | 113 | #Build link response. 114 | message = "Issue with sharing #{playlist_choice} playlist..." 115 | @resources.playlists_list.each do |playlist| 116 | if playlist[0] == playlist_choice 117 | message = playlist[2] 118 | break 119 | end 120 | end 121 | 122 | event = {} 123 | event['event'] = message_create_header(recipient_id) 124 | 125 | message_data = {} 126 | message_data['text'] = message 127 | 128 | message_data['quick_reply'] = {} 129 | message_data['quick_reply']['type'] = 'options' 130 | 131 | options = build_back_option 'playlists' 132 | options += build_home_option 133 | 134 | message_data['quick_reply']['options'] = options 135 | event['event']['message_create']['message_data'] = message_data 136 | event.to_json 137 | end 138 | 139 | def generate_link_list(recipient_id) 140 | 141 | event = {} 142 | event['event'] = message_create_header(recipient_id) 143 | 144 | message_data = {} 145 | message_data['text'] = 'Select a link:' 146 | 147 | message_data['quick_reply'] = {} 148 | message_data['quick_reply']['type'] = 'options' 149 | 150 | options = [] 151 | 152 | @resources.links_list.each do |item| 153 | if item.count > 0 154 | option = {} 155 | option['label'] = "#{BOT_CHAR} " + item[0] 156 | option['metadata'] = "link_choice: #{item[0]}" 157 | option['description'] = item[1] 158 | options << option 159 | end 160 | end 161 | 162 | options += build_home_option('with description') 163 | 164 | message_data['quick_reply']['options'] = options 165 | 166 | event['event']['message_create']['message_data'] = message_data 167 | event.to_json 168 | 169 | end 170 | 171 | def generate_link(recipient_id, link_choice) 172 | 173 | #Build link response. 174 | message = "Issue with displaying #{link_choice}..." 175 | @resources.links_list.each do |link| 176 | if link[0] == link_choice 177 | message = "#{link[2]}\nSummary:\n#{link[3]}" 178 | break 179 | end 180 | end 181 | event = {} 182 | event['event'] = message_create_header(recipient_id) 183 | 184 | message_data = {} 185 | message_data['text'] = message 186 | 187 | message_data['quick_reply'] = {} 188 | message_data['quick_reply']['type'] = 'options' 189 | 190 | options = build_back_option 'links' 191 | options += build_home_option 192 | 193 | message_data['quick_reply']['options'] = options 194 | event['event']['message_create']['message_data'] = message_data 195 | event.to_json 196 | 197 | end 198 | 199 | #Saved for when we have a workaround for getting user location coordinates. 200 | def generate_weather_info(recipient_id, coordinates) 201 | 202 | weather_info = @thirdparty_api.get_current_weather(coordinates[1], coordinates[0]) 203 | 204 | event = {} 205 | event['event'] = message_create_header(recipient_id) 206 | 207 | message_data = {} 208 | message_data['text'] = weather_info 209 | 210 | message_data['quick_reply'] = {} 211 | message_data['quick_reply']['type'] = 'options' 212 | 213 | options = [] 214 | 215 | options += build_home_option 216 | 217 | message_data['quick_reply']['options'] = options 218 | 219 | event['event']['message_create']['message_data'] = message_data 220 | event.to_json 221 | 222 | end 223 | 224 | #Generates Quick Reply for presenting user a Location List via Direct Message. 225 | #https://dev.twitter.com/rest/direct-messages/quick-replies/options 226 | def generate_location_list(recipient_id) 227 | 228 | event = {} 229 | event['event'] = message_create_header(recipient_id) 230 | 231 | message_data = {} 232 | message_data['text'] = "#{BOT_CHAR} Select your area of interest:" 233 | 234 | message_data['quick_reply'] = {} 235 | message_data['quick_reply']['type'] = 'options' 236 | 237 | options = [] 238 | 239 | @resources.locations_list.each do |item| 240 | if item.count > 0 241 | option = {} 242 | option['label'] = "#{BOT_CHAR} " + item[0] 243 | option['metadata'] = "location_choice: #{item[0].strip}" 244 | #option['description'] = 'what is there to say here?' 245 | options << option 246 | end 247 | end 248 | 249 | options += build_home_option 250 | 251 | message_data['quick_reply']['options'] = options 252 | 253 | event['event']['message_create']['message_data'] = message_data 254 | event.to_json 255 | 256 | end 257 | 258 | def generate_location_info(recipient_id, location_name) 259 | 260 | resort_id = 0 261 | @resources.locations_list.each do |location| 262 | if location[0] == location_name 263 | resort_id = location[3] 264 | break 265 | end 266 | end 267 | 268 | resort_info = @thirdparty_api.get_resort_info(resort_id) 269 | 270 | event = {} 271 | event['event'] = message_create_header(recipient_id) 272 | 273 | message_data = {} 274 | message_data['text'] = resort_info 275 | 276 | message_data['quick_reply'] = {} 277 | message_data['quick_reply']['type'] = 'options' 278 | 279 | options = build_back_option 'locations' 280 | options = options + build_home_option #('with_description') 281 | 282 | message_data['quick_reply']['options'] = options 283 | event['event']['message_create']['message_data'] = message_data 284 | event.to_json 285 | 286 | end 287 | 288 | #===================================================================================== 289 | 290 | def generate_greeting 291 | 292 | greeting = "#{BOT_CHAR} Welcome to #{BOT_NAME} (ver. #{VERSION}) #{BOT_CHAR}. Send 'home' for main menu and 'help' for a list of supported commands." 293 | greeting 294 | 295 | end 296 | 297 | def generate_main_message 298 | greeting = '' 299 | greeting = generate_greeting 300 | greeting =+ "#{BOT_CHAR} Thanks for stopping by... #{BOT_CHAR}" 301 | 302 | end 303 | 304 | def message_create_header(recipient_id) 305 | 306 | header = {} 307 | 308 | header['type'] = 'message_create' 309 | header['message_create'] = {} 310 | header['message_create']['target'] = {} 311 | header['message_create']['target']['recipient_id'] = "#{recipient_id}" 312 | 313 | header 314 | 315 | end 316 | 317 | def generate_welcome_message_default 318 | 319 | message = {} 320 | message['welcome_message'] = {} 321 | message['welcome_message']['message_data'] = {} 322 | message['welcome_message']['message_data']['text'] = generate_greeting 323 | 324 | message['welcome_message']['message_data']['quick_reply'] = generate_welcome_options 325 | 326 | message.to_json 327 | 328 | end 329 | 330 | #Users are shown this when returning home... A way to 're-start' dialogs... 331 | #https://dev.twitter.com/rest/reference/post/direct_messages/welcome_messages/new 332 | def generate_welcome_message(recipient_id) 333 | 334 | event = {} 335 | event['event'] = message_create_header(recipient_id) 336 | 337 | message_data = {} 338 | message_data['text'] = "#{BOT_CHAR} Welcome back..." #generate_main_message 339 | 340 | message_data['quick_reply'] = generate_welcome_options 341 | 342 | event['event']['message_create']['message_data'] = message_data 343 | 344 | event.to_json 345 | 346 | end 347 | 348 | def generate_system_info(recipient_id) 349 | 350 | message_text = "#{BOT_CHAR} This is a snow bot (version #{VERSION})... It's kinda simple, kinda not... \n " + 351 | "See here for project code and tutorial: https://github.com/twitterdev/SnowBotDev/wiki. \n" + 352 | "\n" + 353 | "Credits: \n" + 354 | "Snow reports are provided with an API from @SnoCountryCom.\n" + 355 | "Weather data are provided with an API from Weather Underground.\n" 356 | 357 | 358 | #Build DM content. 359 | event = {} 360 | event['event'] = message_create_header(recipient_id) 361 | 362 | message_data = {} 363 | message_data['text'] = message_text 364 | 365 | message_data['quick_reply'] = {} 366 | message_data['quick_reply']['type'] = 'options' 367 | 368 | options = build_home_option 369 | 370 | message_data['quick_reply']['options'] = options 371 | 372 | event['event']['message_create']['message_data'] = message_data 373 | event.to_json 374 | end 375 | 376 | def generate_system_help(recipient_id) 377 | 378 | message_text = "Several commands are supported: \n \n" + 379 | "#{BOT_CHAR} ⇨ Main menu \n send: 'bot', 'home', 'main' \n " + 380 | "#{BOT_CHAR} ⇨ See photo \n send: 'photo', 'pic' \n " + 381 | "#{BOT_CHAR} ⇨ Get resort snow report \n send: 'report', 'resort' \n via http://feeds.snocountry.net/conditions \n " + 382 | "#{BOT_CHAR} ⇨ Learn about snow \n send: 'learn', 'link' \n " + 383 | "#{BOT_CHAR} ⇨ Get playlist \n send: 'playlist', 'music' \n " + 384 | "#{BOT_CHAR} ⇨ Learn about the #{BOT_NAME} \n send: 'about' \n " + 385 | "#{BOT_CHAR} ⇨ Review these commands \n send: 'help' \n " 386 | 387 | #Build DM content. 388 | event = {} 389 | event['event'] = message_create_header(recipient_id) 390 | 391 | message_data = {} 392 | message_data['text'] = message_text 393 | 394 | message_data['quick_reply'] = {} 395 | message_data['quick_reply']['type'] = 'options' 396 | 397 | options = [] 398 | #Not including 'description' option attributes. 399 | 400 | options = build_home_option 401 | 402 | message_data['quick_reply']['options'] = options 403 | 404 | event['event']['message_create']['message_data'] = message_data 405 | event.to_json 406 | end 407 | 408 | #===================================================================================== 409 | 410 | def build_custom_options 411 | 412 | options = [] 413 | 414 | option = {} 415 | option['label'] = "#{BOT_CHAR} See snow picture 📷" 416 | option['description'] = 'Check out a random snow related photo...' 417 | option['metadata'] = 'see_photo' 418 | options << option 419 | 420 | option = {} 421 | option['label'] = "#{BOT_CHAR} Request snow report" 422 | option['description'] = 'SnoCountry reports for select areas.' 423 | option['metadata'] = 'snow_report' 424 | options << option 425 | 426 | option = {} 427 | option['label'] = "#{BOT_CHAR} Learn something new about snow" 428 | option['description'] = 'Other than it is fun to slide on...' 429 | option['metadata'] = 'learn_snow' 430 | options << option 431 | 432 | option = {} 433 | option['label'] = "#{BOT_CHAR} Get geo, weather themed playlist" 434 | option['description'] = 'Carefully curated Spotify playlists...' 435 | option['metadata'] = 'snow_music' 436 | options << option 437 | 438 | options 439 | 440 | end 441 | 442 | def build_default_options 443 | 444 | options = [] 445 | 446 | option = {} 447 | option['label'] = '❓ Learn more about this system' 448 | option['description'] = 'Including a link to underlying code...' 449 | option['metadata'] = 'learn_more' 450 | options << option 451 | 452 | option = {} 453 | option['label'] = '☔ Help' 454 | option['description'] = 'Help with system commands' 455 | option['metadata'] = 'help' 456 | options << option 457 | 458 | option = {} 459 | option['label'] = '⌂ Home' 460 | option['description'] = 'Go back home' 461 | option['metadata'] = "return_home" 462 | options << option 463 | 464 | options 465 | 466 | end 467 | 468 | def build_photo_option 469 | 470 | options = [] 471 | 472 | option = {} 473 | option['label'] = "#{BOT_CHAR} Another 📷 " 474 | option['description'] = '📷Another snow photo' 475 | option['metadata'] = "see_photo" 476 | options << option 477 | 478 | options 479 | 480 | end 481 | 482 | #Types: list choices, going back to list. links, resorts 483 | def build_back_option(type=nil, description=nil) 484 | 485 | options = [] 486 | 487 | option = {} 488 | option['label'] = '⬅ Back' 489 | option['description'] = 'Previous list...' if description 490 | option['metadata'] = "go_back #{type} " 491 | 492 | options << option 493 | 494 | options 495 | 496 | end 497 | 498 | def build_home_option(description=nil) 499 | 500 | options = [] 501 | 502 | option = {} 503 | option['label'] = '⌂ Home' 504 | option['description'] = 'Go back home' if description 505 | option['metadata'] = "return_home" 506 | options << option 507 | 508 | options 509 | 510 | end 511 | 512 | def generate_welcome_options 513 | quick_reply = {} 514 | quick_reply['type'] = 'options' 515 | quick_reply['options'] = [] 516 | 517 | custom_options = [] 518 | custom_options = build_custom_options 519 | custom_options.each do |option| 520 | quick_reply['options'] << option 521 | end 522 | 523 | default_options = [] 524 | default_options = build_default_options 525 | default_options.each do |option| 526 | quick_reply['options'] << option 527 | end 528 | 529 | quick_reply 530 | end 531 | 532 | #============================================================= 533 | 534 | #https://dev.twitter.com/rest/reference/post/direct_messages/welcome_messages/new 535 | def generate_system_maintenance_welcome 536 | 537 | message = {} 538 | message['welcome_message'] = {} 539 | message['welcome_message']['message_data'] = {} 540 | message['welcome_message']['message_data']['text'] = "System going under maintenance... Come back soon..." 541 | 542 | message.to_json 543 | 544 | end 545 | 546 | #https://dev.twitter.com/rest/reference/post/direct_messages/welcome_messages/new 547 | def generate_message(recipient_id, message) 548 | 549 | #Build DM content. 550 | event = {} 551 | event['event'] = message_create_header(recipient_id) 552 | 553 | message_data = {} 554 | message_data['text'] = message 555 | 556 | event['event']['message_create']['message_data'] = message_data 557 | 558 | #TODO: Add home option? options = options + build_home_option 559 | 560 | event.to_json 561 | end 562 | 563 | end 564 | --------------------------------------------------------------------------------