commit 7d5f0cdaf15e78b9085841f72e76ab616f83a430 Author: Marc-AntoineA Date: Sat Feb 20 00:31:13 2021 +0100 Initial commit. diff --git a/mobilizon-agenda.rb b/mobilizon-agenda.rb new file mode 100644 index 0000000..01f2e92 --- /dev/null +++ b/mobilizon-agenda.rb @@ -0,0 +1,338 @@ +require 'json' +require 'down' +require 'fileutils' +require 'date' + +Jekyll::Hooks.register :site, :after_init do |site| + puts "Cleaning #{site.config['mobilizon_cachedir']} folder" + FileUtils.rm_rf(site.config['mobilizon_cachedir']) +end + +module Jekyll + + class MobilizonAgenda < Liquid::Block + + def initialize(tag_name, markup, parse_context) + super + @markup = markup + @attributes = {} + + end + + def fetch_and_thumbnail_image(site, url, cache_dir) + cache_name = url.gsub /[\ :\/\?=]+/, '_' + cache_dir_media = File.join cache_dir, 'media' + cache_path = File.join cache_dir_media, cache_name + + if File.exist? cache_path + return + end + + tempfile = Down.download(url) + + puts "Thumbnailing #{cache_name}" + + image = MiniMagick::Image.open(tempfile.path) + + # Always strip meta + image.strip + image.resize "540x300^" # au moins 540 en largeur et 300 en hauteur + + FileUtils.mkdir_p cache_dir_media unless Dir.exists? cache_dir_media + image.write cache_path + + # https://stackoverflow.com/questions/12124821/how-to-generate-files-from-liquid-blocks-in-jekyll + # Et https://patmurray.co/words/jekyll-open-graph-images + # Add the file to the list of static_files needed to be copied to the _site + site.static_files << Jekyll::StaticFile.new(site, site.source, File.join(site.config['mobilizon_cachedir'], 'media'), cache_name) + end # def fetch_and_thumbnail_image + + def fetch_mobilizon(url, groups) + puts "Mobilizon crawler \n" + + client = Graphlient::Client.new(url, + headers: { + }, + http_options: { + read_timeout: 20, + write_timeout: 30 + } + ) + + $today = DateTime.now().iso8601(2) + + $events = Array.new() + for $group in groups + query_nb_events = client.parse <<~GRAPHQL + query($group: String!) { + group(preferredUsername: $group) { + organizedEvents { + total, + } + } + } + GRAPHQL + response = client.execute query_nb_events, group: $group + $nb_events = response.data.group.organized_events.total + if $nb_events == 0 + next + end + + $nb_pages = ($nb_events / 10.0).ceil + + for $page in 1..$nb_pages + + query = client.parse <<~GRAPHQL + query($page: Int, $group: String!, $afterDatetime: DateTime) { + group(preferredUsername: $group) { + organizedEvents(page: $page, afterDatetime: $afterDatetime){ + elements { + title, + url, + beginsOn, + endsOn, + attributedTo { + avatar { + url, + } + name, + preferredUsername, + }, + description, + onlineAddress, + physicalAddress { + locality, + description, + region + }, + tags { + title, + id, + slug + }, + picture { + url + }, + }, + } + } + } + GRAPHQL + + response = client.execute query, page: $page, group: $group, afterDatetime: $today + + for $event in response.data.group.organized_events.elements + $events.push $event.to_h + end + end + end + $size = $events.size + puts "Crawling ended: #$size events crawled" + return $events + end # def + + def fetch_ics_event(site, url, cache_dir) + cache_name = url.gsub /[\ :\/\?=]+/, '_' + + cache_path = File.join cache_dir, '_ics', cache_name + + if !File.exist? cache_path + puts "Fetching ics #{url}" + tempfile = Down.download(url) + FileUtils.mkdir_p (cache_dir + '/_ics') unless Dir.exists? (cache_dir + '/_ics') + FileUtils.mv tempfile.path, cache_path + end + + cal_file = File.open cache_path + + cals = Icalendar::Calendar.parse cal_file + cal = cals.first + event = cal.events.first + event + end # def fetch_or_ + + def create_icalendar(site, events, pageUrl) + + cache_dir = File.join site.source, site.config['mobilizon_cachedir'] + filename = pageUrl[1..-2].gsub(/[:\/]+/, '_') + '.ics' + cache_path = File.join cache_dir, 'calendar', filename + if File.exist? cache_path + return + end + + puts "Creating icalendar for #{pageUrl}" + cal = Icalendar::Calendar.new + + # ugly + cal.timezone do |t| + t.tzid = "Europe/Paris" + + t.daylight do |d| + d.tzoffsetfrom = "+0100" + d.tzoffsetto = "+0200" + d.tzname = "CEST" + d.dtstart = "19700329T020000" + d.rrule = "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU" + end + + t.standard do |s| + s.tzoffsetfrom = "+0200" + s.tzoffsetto = "+0100" + s.tzname = "CET" + s.dtstart = "19701025T030000" + s.rrule = "FREQ=YEARLY;BYMONTH=01;BYDAY=1SU" + end + end + + for $event in events + if $event['url'] + cal.events.push fetch_ics_event(site, $event['url'] + '/export/ics', cache_dir) + end + end + + cache_dir = File.join cache_dir, 'calendar' + FileUtils.mkdir_p cache_dir unless Dir.exists? cache_dir + + File.open(cache_path, 'w') { |file| + file.write cal.to_ical + } + + site.static_files << Jekyll::StaticFile.new(site, site.source, File.join(site.config['mobilizon_cachedir'], 'calendar'), filename) + end + + def fetch_or_get_cached_events(site) + + cache_dir = File.join site.source, site.config['mobilizon_cachedir'] + + url = site.config["mobilizon_url"] + "/api" + cache_name = url.gsub /[:\/]+/, '_' + cache_dir_requests = File.join cache_dir, '_requests' + cache_path = File.join cache_dir_requests, cache_name + + if File.exist? cache_path + file = File.open(cache_path) + cache = JSON.load file + file.close + else + cache = fetch_mobilizon(url, site.config['mobilizon_whitelist']) + FileUtils.mkdir_p cache_dir_requests unless Dir.exists? cache_dir_requests + File.open(cache_path, 'w') { |file| + file.write JSON.pretty_generate(cache) + } + file= File.open(cache_path) + cache = JSON.load file + + for $event in cache + if $event['picture'] + fetch_and_thumbnail_image(site, $event['picture']['url'], cache_dir) + end + if $event['attributedTo']['avatar'] + fetch_and_thumbnail_image(site, $event['attributedTo']['avatar']['url'], cache_dir) + end + end + end + + return cache + end # def generate + + def filter_events(events, tags_and_organizers) + return events.select { |event| (tags_and_organizers.include? event['attributedTo']['preferredUsername']) || !(tags_and_organizers & (event['tags'].map { |tag| tag['slug'].downcase })).empty? || !(tags_and_organizers & (event['tags'].map { |tag| tag['title'].downcase })).empty? } + end # def filter_events + + def change_to_timezone(string_date, timezone) + date = DateTime.iso8601(string_date) + .in_time_zone(timezone) + date.iso8601(2) + end + + def render(context) + context.registers[:ical] ||= Hash.new(0) + + result = [] + context.stack do + site = context.registers[:site] + return unless site.config['mobilizon_fetch'] == true + tags_and_organizers = context[@markup.strip()].split(',') + + events = fetch_or_get_cached_events(site) + + events = filter_events(events, tags_and_organizers) + events = events.sort_by{ |event| event['beginsOn'] } + + create_icalendar(site, events, context.registers[:page]['url']) + + event_count = events.length + if event_count == 0 + context["forloop"] = { + "name" => "ical", + "length" => event_count, + "index" => 1, + "index0" => 0, + "rindex" => event_count, + "rindex0" => event_count - 1, + "first" => true, + "last" => true, + } + result << nodelist.map do |n| + if n.respond_to? :render + n.render(context) + else + n + end + end.join + end + + events.each_with_index do |event, index| + context["event"] = event + context["event"]["index"] = index + context["event"]["beginsOn"] = change_to_timezone context["event"]["beginsOn"], site.config["mobilizon_timezone"] + context["event"]["endsOn"] = change_to_timezone context["event"]["endsOn"], site.config["mobilizon_timezone"] + if context["event"]["picture"] + context["event"]["thumbnailurl"] = URI::encode(File.join site.config['mobilizon_cachedir'], "media", context["event"]["picture"]["url"].gsub(/[\ :\/\?=]+/, '_')) + end + if context["event"]["physicalAddress"] && context["event"]["physicalAddress"]["locality"] + if context["event"]["physicalAddress"]["description"] == context["event"]["physicalAddress"]["locality"] + context["event"]["location"] = context["event"]["physicalAddress"]["locality"] + ", " + context["event"]["physicalAddress"]["region"] + else + context["event"]["location"] = context["event"]["physicalAddress"]["description"] + ", " + context["event"]["physicalAddress"]["locality"] + ", " + context["event"]["physicalAddress"]["region"] + end + end + if context["event"]["attributedTo"] && context["event"]["attributedTo"]["avatar"] && context["event"]["attributedTo"]["avatar"]["url"] + context["event"]["organizerAvatar"] = URI::encode( + File.join(site.config['mobilizon_cachedir'], + "media", + context["event"]["attributedTo"]["avatar"]["url"].gsub(/[\ :\/\?=]+/, '_') + ) + ) + end + if context["event"]["attributedTo"] && context["event"]["attributedTo"]["name"] + context["event"]["organizer"] = context["event"]["attributedTo"]["name"] + context["event"]["groupUrl"] = site.config['mobilizon_url'] + '/@' + context["event"]["attributedTo"]["preferredUsername"] + end + + context["forloop"] = { + "name" => "ical", + "length" => event_count, + "index" => index + 1, + "index0" => index, + "rindex" => event_count - index, + "rindex0" => event_count - index - 1, + "first" => (index == 0), + "last" => (index == event_count - 1), + } + + result << nodelist.map do |n| + if n.respond_to? :render + n.render(context) + else + n + end + end.join + end + end # context stack + result + end # def render + end +end + +Liquid::Template.register_tag('mobilizonAgenda', Jekyll::MobilizonAgenda) diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..da3f568 --- /dev/null +++ b/readme.md @@ -0,0 +1,45 @@ +# Script to include Mobilizon into Jekyll-website + +This plugin fetches incoming events for a list of groups in a given mobilizon instance (through graphql api). +Images are fetched too and thumbnailed with `minimagick`. It means that no requests are made to mobilizon except during website compilations. + +You can create many agendas based on `groups` and `tags` through the `mobilizonAgenda` Jekyll-tag + +**Example** You want to display all events from group `my-group` and all events with tag `my-tag` and all events with tag `my_awesome_tag` : +```html +{% assign options="my-group,my_tag,my_awesome_tag" %} +{% mobilizonAgenda options %} + +{% if forloop.length == 0 %} +

We did not find any event.

+{% else %} +{% include event.html + title=event.title + location=event.location + start_time=event.beginsOn + end_time=event.endsOn + description=event.description + url=event.url + thumbnail=event.thumbnailurl + organizerAvatar=event.organizerAvatar + organizer=event.organizer + groupUrl=event.groupUrl + %} +{% endif %} +{% endmobilizonAgenda %} +``` + +## How to use it? + +Add these lines into your `_config.yml` +```yml +mobilizon_fetch: true # false if you want to deactivate it +mobilizon_url: "https://mobilizon.fr" +mobilizon_cachedir: "mobilizon" # the name of the local folder used to cache the results +mobilizon_timezone: "Europe/Paris" # used to convert the dates +mobilizon_whitelist: # list of the mobilizon groups you want to display on your website + - my_group + - my_second_group +``` + +Add `mobilizon-agenda.rb` into your `_plugins` folder.