Skip to content

Commit

Permalink
feat: Add Resident Advisor support (#2579)
Browse files Browse the repository at this point in the history
  • Loading branch information
kimadactyl authored Aug 28, 2024
1 parent f7eac59 commit cbb3a64
Show file tree
Hide file tree
Showing 8 changed files with 382 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/jobs/calendar_importer/calendar_importer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CalendarImporter::CalendarImporter
CalendarImporter::Parsers::Ics,
CalendarImporter::Parsers::ManchesterUni,
CalendarImporter::Parsers::Meetup,
CalendarImporter::Parsers::ResidentAdvisor,
CalendarImporter::Parsers::Squarespace,
CalendarImporter::Parsers::Ticketsolve,

Expand Down
55 changes: 55 additions & 0 deletions app/jobs/calendar_importer/events/resident_advisor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module CalendarImporter::Events
class ResidentAdvisor < Base
def initialize(event)
super
@event = event
end

def uid
@event['id']
end

def summary
@event['title']
end

def description
@event['content'] || ''
end

def place
'' # N/A
end

def publisher_url
"https://ra.co#{@event['contentUrl']}"
end

def location
return @event['venue']['address'] if @event['venue']
end

def dtstart
@event['startTime']
end

def dtend
@event['endTime']
end

def occurrences_between(*)
[Dates.new(dtstart, dtend)]
end

def online_event?
return # I don't think RA supports online events

# return unless @event['is_online_event']

# online_address = OnlineAddress.find_or_create_by(url: @event['link'], link_type: 'indirect')
# online_address.id
end
end
end
4 changes: 3 additions & 1 deletion app/jobs/calendar_importer/parsers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ def self.safely_parse_json(string)
# is not 200 (even following redirects) then raise the correct
# exception with an appropriate message
def self.read_http_source(url, follow_redirects: true)
response = HTTParty.get(url, follow_redirects: follow_redirects)
# User-Agent is currently set to make Resident Advisor happy, but this is also more "honest".
# It may be this method needs per-vendor headers
response = HTTParty.get(url, follow_redirects: follow_redirects, headers: { 'User-Agent': 'Httparty' })
return response.body if response.success?

msg = "The source URL could not be read (code=#{response.code})"
Expand Down
104 changes: 104 additions & 0 deletions app/jobs/calendar_importer/parsers/resident_advisor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

module CalendarImporter::Parsers
class ResidentAdvisor < Base
NAME = 'Resident Advisor'
KEY = 'residentadvisor'
DOMAINS = %w[ra.co].freeze
RA_ENDPOINT = 'https://ra.co/graphql'
RA_REGEX = %r{^https://ra\.co/(promoters|clubs)/(\d+)$} # Accept club or promoter URLs

def self.allowlist_pattern
RA_REGEX
end

def download_calendar
ra_entity = ra_entity(@url)
return unless ra_entity

if ra_entity[0] == :promoters
get_promoter_events(ra_entity[1])
elsif ra_entity[0] == :clubs
get_club_events(ra_entity[1])
end
end

def import_events_from(data)
data.map { |d| CalendarImporter::Events::ResidentAdvisor.new(d) }
end

# Converts an RA URL into [(promoter OR club), (id of entity)]
def ra_entity(url)
result = RA_REGEX.match(url)
return false unless result

[result[1].to_sym, result[2].to_i]
end

def get_promoter_events(id)
# LATEST query is discovered via introspection.
# I think it gives the next 10 events.
query = <<~GRAPHQL
promoter(id: #{id}) {
id
name
email
events(type: LATEST) {
id
title
content
startTime
endTime
contentUrl
venue {
id
name
address
}
}
}
GRAPHQL

events = query_graphql_endpoint(query)
events['data']['promoter']['events']
end

def get_club_events(id)
# LATEST query is discovered via introspection.
# I think it gives the next 10 events.
query = <<~GRAPHQL
venue(id: #{id}) {
id
name
address
events(type: LATEST) {
id
title
content
startTime
endTime
contentUrl
}
}
GRAPHQL

events = query_graphql_endpoint(query)
events['data']['venue']['events']
end

# Send a POST request to the GraphQL endpoint
# TODO: Migrate into a generic GraphQL base class
def query_graphql_endpoint(query)
HTTParty.post(RA_ENDPOINT,
body: postify_query_string(query),
headers: { 'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0' })
end

# Massage a GraphQL query into a post request body
def postify_query_string(query)
"{\"query\": \"{#{query}}\"}".gsub("\n", '')
end
end
end
37 changes: 37 additions & 0 deletions app/jobs/creating_an_importer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# How the importer works

Calendars can be imported through the following rake tasks:

```
rails events:import_all_calendars # Imports all calendars - usually in dev environment
rails events:import_calendars[:id] # Imports one calendar - usually in a dev environment
rails events:scan_for_calendars_needing_import # Imports only updated calendars - run as a cronjob in a production environment
```

They can also be triggered in the Calendar interface in the web admin.

All of these tasks create `CalendarImporterJob`s (/app/jobs/calendar_importer_job.rb). This job is how all external URLs get turned into Events and imported into PlaceCal, with Calendars being effectively the configuration for how to import them.

1. `CalendarImporterJob` looks up each Calendar. It creates a...
2. `CalendarImporter::CalendarImporterTask`, which identifies the calendar type and attempts to load it using a...
3. `CalendarImporter::Parsers::MyParser`, which queries a URL and sends the data from it to...
4. `CalendarImporter::EventResolver`, which analyses event data and creates one or more...
5. `CalendarImporter::Events::MyParser`, which attempts to load an event into PlaceCal

## Adding a new importer

Importers require two parts:

1. A **CalendarImporter::Parsers**, which reads a remote URL and turns it into something Rails can work with
2. A **CalendarImporter::Events**, which takes the data and creates one or more Events inside PlaceCal.

### CalendarImporter::Parsers

1. Create `/app/jobs/calendar_importer/parsers/my_parser.rb` and link it from `/app/jobs/calendar_importer/calendar_importer.rb`.
2. Implement a `#download_calendar` method that returns event data.
3. Implement an `#import_events_from(data)` method that invokes a `CalendarImporter::Events`.

### CalendarImporter::Events

1. Create `/app/jobs/calendar_importer/events/my_parser.rb`.
2. Add methods to map your retrieved events onto PlaceCal Events model (nb: we should define this more clearly)
56 changes: 56 additions & 0 deletions test/fixtures/vcr_cassettes/ra_club.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit cbb3a64

Please sign in to comment.