diff --git a/Manifest.txt b/Manifest.txt index af988a3..4e12b51 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -12,6 +12,7 @@ bin/setup coffeeoutside.gemspec config.example.yaml lib/coffeeoutside.rb +lib/coffeeoutside/dispatchers/bluesky.rb lib/coffeeoutside/dispatchers/dispatcher.rb lib/coffeeoutside/dispatchers/ical.rb lib/coffeeoutside/dispatchers/json.rb diff --git a/coffeeoutside.gemspec b/coffeeoutside.gemspec index 7b0c166..18812a5 100644 --- a/coffeeoutside.gemspec +++ b/coffeeoutside.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency "http", "= 4.0.0" + spec.add_dependency "http", ">= 5.0" spec.add_dependency "icalendar", "= 2.11.0" spec.add_dependency "openweathermap", "= 0.2.3" spec.add_dependency "rss", "= 0.2.9" diff --git a/config.example.yaml b/config.example.yaml index d67e7bd..6f3e009 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,10 +1,10 @@ production: false dispatchers: - service: - consumer_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - consumer_key: xxxxxxxxxxxxxxxxxxxxxxxxxx + mastodon: token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - token_secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + bluesky: + identifier: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx openweathermap: city_id: 5913490 api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx diff --git a/lib/coffeeoutside.rb b/lib/coffeeoutside.rb index 6837c1d..f78aef2 100644 --- a/lib/coffeeoutside.rb +++ b/lib/coffeeoutside.rb @@ -53,8 +53,10 @@ def main } CoffeeOutside::Dispatchers.constants.each do |d| - dispatch.merge!(config.dispatchers[d.to_s]) if config.dispatchers[d.to_s] - ::CoffeeOutside::Dispatchers.const_get(d).new(dispatch).notify + # Get dispatcher info from config.yaml when lowercase + dispatcher_config = config.dispatchers&.fetch(d.to_s.downcase, nil) || config.dispatchers&.fetch(d.to_s, nil) || {} + params = dispatch.merge(dispatcher_config) + ::CoffeeOutside::Dispatchers.const_get(d).new(params).notify end end end diff --git a/lib/coffeeoutside/dispatchers/bluesky.rb b/lib/coffeeoutside/dispatchers/bluesky.rb new file mode 100644 index 0000000..ea3d4d8 --- /dev/null +++ b/lib/coffeeoutside/dispatchers/bluesky.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: false + +# rbs_inline: enabled + +require_relative "dispatcher" +require "json" +require "http" +require "time" + +module CoffeeOutside + module Dispatchers + class Bluesky < DispatcherBase + def notify_production + return "Could not find identifier, skipping Bluesky dispatcher" unless @params["identifier"] + return "Could not find password, skipping Bluesky dispatcher" unless @params["password"] + + host = @params["host"] || "https://bsky.social" + + # Create session to authenticate and get access JWT + session_url = "#{host}/xrpc/com.atproto.server.createSession" + session_resp = HTTP.post(session_url, json: { + identifier: @params["identifier"], + password: @params["password"] + }) + + raise "Failed to authenticate with Bluesky: #{session_resp.body}" unless session_resp.status.success? + + session_data = JSON.parse(session_resp.body.to_s) + access_jwt = session_data["accessJwt"] + did = session_data["did"] + + msg = location_skeet_msg + record = { + "$type": "app.bsky.feed.post", + text: msg, + createdAt: Time.now.utc.iso8601 + } + f = facets(msg) + record[:facets] = f unless f.empty? + + # Post the skeet using the session JWT + post_url = "#{host}/xrpc/com.atproto.repo.createRecord" + post_resp = HTTP.headers({ Authorization: "Bearer #{access_jwt}" }) + .post(post_url, json: { + repo: did, + collection: "app.bsky.feed.post", + record: record + }) + + raise "Failed to post to Bluesky: #{post_resp.body}" unless post_resp.status.success? + end + + def notify_debug + puts "Bluesky identifier = #{@params["identifier"]}" + msg = location_skeet_msg + puts msg + f = facets(msg) + puts "Facets: #{JSON.generate(f)}" unless f.empty? + puts "\n" + end + + def facets(text) + facets_list = [] + + # Identify links + text.scan(%r{https?://[^\s,)]+}) do |url| + m = Regexp.last_match + b_start = text[0...m.begin(0)].bytesize + b_end = b_start + url.bytesize + facets_list << { + index: { byteStart: b_start, byteEnd: b_end }, + features: [{ "$type": "app.bsky.richtext.facet#link", uri: url }] + } + end + + # Identify hashtags + text.scan(/#[a-zA-Z0-9_]+/) do |tag| + m = Regexp.last_match + b_start = text[0...m.begin(0)].bytesize + b_end = b_start + tag.bytesize + facets_list << { + index: { byteStart: b_start, byteEnd: b_end }, + features: [{ "$type": "app.bsky.richtext.facet#tag", tag: tag[1..] }] + } + end + + facets_list + end + + def location_skeet_msg #: String + str = "This week's #CoffeeOutside: #{@location.name}" + str << " (#{@location.location_hint})" if @location.location_hint + if @location.url + str << " #{@location.url}" + elsif @location.map_url + str << " #{@location.map_url}" + end + str << " (#{@location.address})" if @location.address + str << ", see you there! #yycbike" + str + end + end + end +end