Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added discord webhook functionality #8

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ gem 'rest-client'
gem 'sentry-ruby'
gem 'slack-ruby-client'
gem 'twitter'
gem 'discordrb'

# browser automation
gem 'ferrum'
Expand Down
29 changes: 28 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ GEM
cliver (0.3.2)
concurrent-ruby (1.1.8)
diff-lcs (1.4.4)
discordrb (3.4.0)
discordrb-webhooks (~> 3.3.0)
ffi (>= 1.9.24)
opus-ruby
rest-client (>= 2.0.0)
websocket-client-simple (>= 0.3.0)
discordrb-webhooks (3.3.0)
rest-client (>= 2.1.0.rc1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
equalizer (0.0.11)
event_emitter (0.2.6)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
Expand All @@ -23,6 +32,7 @@ GEM
concurrent-ruby (~> 1.1)
websocket-driver (>= 0.6, < 0.8)
ffi (1.15.0)
ffi (1.15.0-x64-mingw32)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
Expand Down Expand Up @@ -52,6 +62,10 @@ GEM
nokogiri (1.11.2)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.2-x64-mingw32)
racc (~> 1.4)
opus-ruby (1.0.1)
ffi
public_suffix (4.0.6)
racc (1.5.2)
rake (13.0.3)
Expand All @@ -61,6 +75,12 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rest-client (2.1.0-x64-mingw32)
ffi (~> 1.9)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
Expand Down Expand Up @@ -104,14 +124,21 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unf_ext (0.0.7.7-x64-mingw32)
websocket (1.2.9)
websocket-client-simple (0.3.0)
event_emitter
websocket
websocket-driver (0.7.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)

PLATFORMS
ruby
x64-mingw32

DEPENDENCIES
discordrb
ferrum
nokogiri
redis
Expand All @@ -122,4 +149,4 @@ DEPENDENCIES
twitter

BUNDLED WITH
2.2.3
2.2.16
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,19 @@ runtime. To enable tweeting, provide the following:
* TWITTER_CONSUMER_KEY
* TWITTER_CONSUMER_SECRET

This bot can also be configured to send slack notifications to a channel by
This bot can also be configured to send Slack notifications to a channel by
setting the following environment variables:

* SLACK_API_TOKEN
* SLACK_CHANNEL
* SLACK_USERNAME
* SLACK_ICON

It can also send Discord notifications to a channel by setting the following
environment variables:

* DISCORD_TOKEN

Additional configuration can be done with the following:

* SENTRY_DSN - sets up error handling with [Sentry](https://sentry.io)
Expand Down
53 changes: 53 additions & 0 deletions lib/discord.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'discordrb/webhooks'

class FakeDiscord
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FakeDiscord class should respond to the same commands as the real class, which from looking below are execute and then content = to set the text. Is there an easier way than setting execute and using the webhook API? Usually there's a REST-based way to send messages, but I'm not familiar with Discord so maybe not.


def initialize(logger)
@logger = logger
end

def update(str)
@logger.info "[FakeDiscord]: #{str}"
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this class is missing an end statement, which may be causing some of the name errors. In Ruby, each class and function definition must have a corresponding end to close it out.


class DiscordClient

def initialize(logger)
@logger = logger
@discord = if ENV['ENVIRONMENT'] != 'test' && env_keys_exist?
Discordrb::Webhooks::Client.new(url: ENV['DISCORD_WEBHOOK_URL']).freeze
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the environment keys don't exist, we should instantiate a FakeDiscord instance here which just logs the output instead of sending to discord. Very useful for testing purposes.

end
end

def env_keys_exist?
ENV['DISCORD_WEBHOOK_URL']
end

def send(clinic)
@logger.info "[DiscordClient] Sending message for #{clinic.title} (#{clinic.new_appointments} new appointments)"
text = clinic.discord_text
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BaseClinic class will need to have discord_text added to it, it can probably be similar to what the slack text looks like.

if text.is_a?(Array)
text.each { |t| @discord.execute do |builder|
builder.content = text
end
}
end
@discord.execute do |builder|
builder.content = text
end
end

rescue => e
@logger.error "[DiscordClient] error: #{e}"
raise e unless ENV['ENVIRONMENT'] == 'production' || ENV['ENVIRONMENT'] == 'staging'

Sentry.capture_exception(e)
end

def post(clinics)
clinics.filter(&:should_discord_message?).each do |clinic|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseClinic will also need to have should_discord_message? defined there. It should be similar to the logic for whether to slack. The &:should_discord_message? is short for filter { |clinic| clinic.should_discord_message? }.

send(clinic)
clinic.save_message_time
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should ensure that there are newlines at the end of each file too.

4 changes: 2 additions & 2 deletions lib/twitter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def tweet(clinic)
else
@twitter.update(text)
end
end
Copy link
Member

@dpca dpca Apr 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should stay the same, moving the end to here may change the behavior.


rescue => e
@logger.error "[TwitterClient] error: #{e}"
Expand All @@ -54,5 +55,4 @@ def post(clinics)
tweet(clinic)
clinic.save_tweet_time
end
end
end
end
3 changes: 3 additions & 0 deletions run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative 'lib/storage'
require_relative 'lib/slack'
require_relative 'lib/twitter'
require_relative './lib/discord'

# Sites
require_relative 'lib/sites/ma_immunizations'
Expand Down Expand Up @@ -98,6 +99,7 @@ def main(opts)
storage = Storage.new
slack = SlackClient.new(logger)
twitter = TwitterClient.new(logger)
discord = DiscordClient.new(logger)

logger.info "[Main] Update frequency is set to every #{UPDATE_FREQUENCY} seconds"

Expand All @@ -115,6 +117,7 @@ def main(opts)
all_clinics(storage, logger, **opts) do |clinics|
slack.post(clinics)
twitter.post(clinics)
discord.post(clinics)

clinics.each(&:save_appointments)
end
Expand Down
80 changes: 80 additions & 0 deletions spec/discord_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require 'logger'
require_relative '../lib/discord'
require_relative './mock_clinic'

describe DiscordClient do
let(:discord) { DiscordClient.new(Logger.new('/dev/null')) }

describe '#discord' do
it 'calls the discord "update" method' do
mock_discord = double('Discord')
mock_clinic = double('Clinic', title: 'Test clinic', new_appointments: 1)
expect(FakeDiscord).to receive(:new).and_return(mock_discord)
expect(mock_discord).to receive(:update).with('test message')
expect(mock_clinic).to receive(:discord_text).and_return('test message')
discord.send(mock_clinic)
end
end

describe '#should_discord_message?' do
it 'returns true if the clinic has more than 10 new appointments' do
mock_clinic = MockClinic.new(appointments: 100, new_appointments: 100)
expect(mock_clinic.should_discord_message?).to be_truthy
end

it 'returns false if the clinic has no link' do
mock_clinic = MockClinic.new(appointments: 100, new_appointments: 100, link: nil)
expect(mock_clinic.should_discord_message?).to be_falsy
end

it 'returns false if the clinic has fewer than 10 appointments' do
mock_clinic = MockClinic.new(appointments: 9, new_appointments: 100)
expect(mock_clinic.should_discord_message?).to be_falsy
end

it 'returns false if the clinic has fewer than 5 new appointments' do
mock_clinic = MockClinic.new(appointments: 100, new_appointments: 4)
expect(mock_clinic.should_discord_message?).to be_falsy
end

it 'returns false if the clinic has posted recently' do
mock_clinic = MockClinic.new(appointments: 100, new_appointments: 100, last_posted_time: (Time.now - 60).to_s)
expect(mock_clinic.should_discord_message?).to be_falsy
end
end

describe '#post' do
it 'only sends about clinics that should post' do
valid_clinic = MockClinic.new(appointments: 100, new_appointments: 100)
invalid_clinic = MockClinic.new(appointments: 0, new_appointments: 0)
expect(discord).to receive(:message).with(valid_clinic)
expect(discord).not_to receive(:message).with(invalid_clinic)
expect(valid_clinic).to receive(:save_message_time)
expect(invalid_clinic).not_to receive(:save_message_time)
discord.post([valid_clinic, invalid_clinic])
end

it "doesn't care about the clinic order" do
valid_clinic = MockClinic.new(appointments: 100, new_appointments: 100)
invalid_clinic = MockClinic.new(appointments: 0, new_appointments: 0)
expect(discord).to receive(:message).with(valid_clinic)
expect(discord).not_to receive(:message).with(invalid_clinic)
expect(valid_clinic).to receive(:save_message_time)
expect(invalid_clinic).not_to receive(:save_message_time)
discord.post([invalid_clinic, valid_clinic])
end

it 'works with no clinics' do
expect { discord.post([]) }.not_to raise_exception
end
end

describe '#discord_text' do
it 'posts about appointments with a link' do
mock_clinic = MockClinic.new(title: 'myclinic', appointments: 100, new_appointments: 20)
expect(mock_clinic.discord_text).to eq(
'100 appointments available at myclinic. Check eligibility and sign up at clinicsite.com'
)
end
end
end