From 5d052ab7953afa5d9518f28ae31ebc71928fe5f5 Mon Sep 17 00:00:00 2001
From: Thomas Hackl <hackl@data-quest.de>
Date: Fri, 10 Jun 2022 08:33:40 +0200
Subject: [PATCH] process news posting to Matrix via cronjob so that news are
 pushed when their publish date is reached and not immediately

---
 MatrixPlugin.php                              |  31 +++--
 MatrixPostNewsCronjob.php                     | 120 ++++++++++++++++++
 controllers/matrix_chat.php                   |   7 +-
 migrations/05_post_news_to_matrix_cronjob.php |  53 ++++++++
 models/MatrixRoom.php                         |   2 +-
 plugin.manifest                               |   2 +-
 6 files changed, 199 insertions(+), 16 deletions(-)
 create mode 100644 MatrixPostNewsCronjob.php
 create mode 100644 migrations/05_post_news_to_matrix_cronjob.php

diff --git a/MatrixPlugin.php b/MatrixPlugin.php
index 92ab523..6daf2e1 100644
--- a/MatrixPlugin.php
+++ b/MatrixPlugin.php
@@ -32,8 +32,8 @@ class MatrixPlugin extends StudIPPlugin implements StandardPlugin
         NotificationCenter::addObserver($this, 'uninvite', 'CourseMemberDidDelete');
         NotificationCenter::addObserver($this, 'unregister', 'UserDidDelete');
         NotificationCenter::addObserver($this, 'deleteRoom', 'CourseDidDelete');
-        NotificationCenter::addObserver($this, 'postNews', 'StudipNewsDidCreate');
-        NotificationCenter::addObserver($this, 'postNews', 'StudipNewsDidUpdate');
+        NotificationCenter::addObserver($this, 'updateCronjob', 'StudipNewsWillUpdate');
+        NotificationCenter::addObserver($this, 'updateCronjob', 'StudipNewsDidDelete');
 
         $this->addScript('assets/javascripts/matrixchat.js');
         $this->addStylesheet('assets/stylesheets/matrixchat.scss');
@@ -94,6 +94,7 @@ class MatrixPlugin extends StudIPPlugin implements StandardPlugin
     public function getMetadata()
     {
         return [
+            'pluginname' => dgettext('matrix', 'Matrix-Chat'),
             'summary' => dgettext('matrix', 'Chat via Matrix'),
             'description' => dgettext('matrix', 'Matrix-Chaträume für Veranstaltungen'),
             'category' => _('Kommunikation und Zusammenarbeit'),
@@ -194,22 +195,28 @@ class MatrixPlugin extends StudIPPlugin implements StandardPlugin
     }
 
     /**
-     * Auto-post course news to associated Matrix room if configured.
+     * Remove news from entries relevant for cronjob.
      *
      * @param string $event
      * @param StudipNews $news
      */
-    public function postNews($event, $news)
+    public function updateCronjob($event, $news)
     {
-        foreach ($news->news_ranges as $range) {
-            if ($range->course && MatrixRoom::hasRoom($range->course->id)) {
-                MatrixClient::get()->postMessage(
-                    MatrixAccount::requireSystemAccount(),
-                    MatrixRoom::find($range->course->id)->getLinkedRoom(),
-                    'Ankündigung: ' . $news->topic . '' . strip_tags($news->body),
-                    '<strong>Ankündigung: ' . $news->topic . '</strong><br>' . $news->body
+        switch ($event) {
+            case 'StudipNewsWillUpdate':
+                if ($news->isFieldDirty('date')) {
+                    DBManager::get()->execute(
+                        "UPDATE `matrix_upcoming_news` SET `publish_at` = :publish WHERE `news_id` = :id",
+                        ['publish' => $news->date, 'id' => $news->id]
+                    );
+                }
+                break;
+            case 'StudipNewsDidDelete':
+                DBManager::get()->execute(
+                    "DELETE FROM `matrix_upcoming_news` WHERE `news_id` = :id",
+                    ['id' => $news->id]
                 );
-            }
+                break;
         }
     }
 
diff --git a/MatrixPostNewsCronjob.php b/MatrixPostNewsCronjob.php
new file mode 100644
index 0000000..d4ac57f
--- /dev/null
+++ b/MatrixPostNewsCronjob.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Class MatrixPostNewsCronjob
+ * Cronjob that checks if a course with attached Matrix chatroom has any news, registers them as "to do"
+ * and posts the news content when the news are published in Stud.IP. Publishing news happens at a date
+ * given per news entry, so this cannot be done "live" after the news has been created.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Thomas Hackl <hackl@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Matrix
+ */
+
+require_once(__DIR__ . '/vendor/libpatrix/MatrixClient.php');
+
+class MatrixPostNewsCronjob extends CronJob
+{
+    public static function getName()
+    {
+        return dgettext('matrix', 'Aktuelle Ankündigungen in Matrixraum veröffentlichen');
+    }
+
+    public static function getDescription()
+    {
+        return dgettext('matrix', 'Dieser Cronjob veröffentlicht eine Ankündigung im zugehörigen ' .
+            'Matrixraum, wenn sie auch in der entsprechenden Veranstaltung öffentlich wird.');
+    }
+
+    public static function getParameters()
+    {
+        return [
+            'verbose' => [
+                'type'        => 'boolean',
+                'default'     => false,
+                'status'      => 'optional',
+                'description' => _('Sollen Ausgaben erzeugt werden'),
+            ],
+        ];
+    }
+
+    public function execute($last_result, $parameters = [])
+    {
+        /*
+         * First, check if there are news that need to be monitored until they are published.
+         * This means getting all news
+         * - that belong to a course with attaached Matrix room
+         * - that haven't expired
+         * - that haven't already been posted
+         * and inserting them into the monitoring table or updating them accordingly.
+         */
+        DBManager::get()->execute("INSERT INTO `matrix_upcoming_news`
+			(
+                SELECT DISTINCT n.`news_id`, r.`range_id`, n.`date`, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() FROM `news` n
+                        JOIN `news_range` r USING (`news_id`)
+                        JOIN `matrix_rooms` m ON (m.`range_id` = r.`range_id`)
+                    WHERE UNIX_TIMESTAMP() <= n.`date` + n.`expire`
+            )
+            ON DUPLICATE KEY UPDATE `publish_at` = VALUES(`publish_at`), `chdate` = UNIX_TIMESTAMP()",
+            ['now' => time()]
+        );
+
+        /*
+         * Now walk through entries in monitoring table and push each entry
+         * to the corresponding chatroom if publish_date is reached.
+         */
+        $news = DBManager::get()->fetchFirst(
+            "SELECT `news_id` FROM `matrix_upcoming_news` WHERE `publish_at` <= UNIX_TIMESTAMP() AND `posted` = 0"
+        );
+
+        // Verbose output
+        if ($parameters['verbose']) {
+            echo "Got news_ids:\n";
+            print_r($news);
+        }
+        foreach (StudipNews::findMany($news) as $one) {
+            foreach ($one->news_ranges as $range) {
+                $success = false;
+                if ($range->course && MatrixRoom::hasRoom($range->course->id)) {
+                    $room = MatrixRoom::find($range->course->id);
+
+                    $success = MatrixClient::get()->postMessage(
+                        MatrixAccount::requireSystemAccount(),
+                        $room->getLinkedRoom(),
+                        'Ankündigung: ' . $one->topic . ' ' . strip_tags($one->body),
+                        '<strong>Ankündigung: ' . $one->topic . '</strong><br>' . $one->body
+                    );
+
+                    // Mark entry as done after message has been posted to Matrix room successfully.
+                    if ($success) {
+                        // Verbose output
+                        if ($parameters['verbose']) {
+                            echo sprintf(
+                                "Posted news entry $1%s to Matrix room $2%s in course $3%s.\n",
+                                $news->id, $room->matrix_room_id, $room->range_id
+                            );
+                        }
+
+                        DBManager::get()->execute(
+                            "UPDATE `matrix_upcoming_news` SET `posted` = 1 WHERE `news_id` = :id AND `range_id` = :range",
+                            ['id' => $one->id, 'range' => $range->course->id]
+                        );
+                    }
+                }
+            }
+        }
+
+        // Finally: cleanup news entries that are already expired and need not be considered anymore.
+        DBManager::get()->execute("DELETE FROM `matrix_upcoming_news` WHERE `news_id` IN (
+                SELECT DISTINCT n.`news_id` FROM `news` n
+                    JOIN `matrix_upcoming_news` m USING (`news_id`)
+                WHERE n.`date` + n.`expire` < UNIX_TIMESTAMP()
+            )"
+        );
+
+    }
+}
diff --git a/controllers/matrix_chat.php b/controllers/matrix_chat.php
index 83836ff..5c1507f 100644
--- a/controllers/matrix_chat.php
+++ b/controllers/matrix_chat.php
@@ -79,8 +79,11 @@ class MatrixChatController extends AuthenticatedController
                     $this->hasToCreate = false;
 
                     if ($this->account) {
-                        //$room->requireMembership($this->account->getLinkedAccount());
-                        $invited = $room->requireInvitation($this->account->getLinkedAccount());
+                        if (Config::get()->MATRIX_AUTO_CREATE_ACCOUNTS) {
+                            $room->requireMembership($this->account->getLinkedAccount());
+                        } else {
+                            $invited = $room->requireInvitation($this->account->getLinkedAccount());
+                        }
                     }
                 }
             }
diff --git a/migrations/05_post_news_to_matrix_cronjob.php b/migrations/05_post_news_to_matrix_cronjob.php
new file mode 100644
index 0000000..8e37b54
--- /dev/null
+++ b/migrations/05_post_news_to_matrix_cronjob.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Class PostNewsToMatrixCronjob
+ * Registers a cronjob for posting news at publish time
+ * to the corresponding Matrix chatroom.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * @author      Thomas Hackl <hackl@data-quest.de>
+ * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category    Matrix
+ */
+
+require_once(__DIR__ . '/../MatrixPostNewsCronjob.php');
+
+class PostNewsToMatrixCronjob extends Migration
+{
+
+    public function description()
+    {
+        return 'Registers a cronjob for posting news at publish time to the corresponding Matrix chatroom.';
+    }
+
+    public function up()
+    {
+        /**
+         * Collect news entries that need to be published to a Matrix chatroom.
+         */
+        DBManager::get()->execute("CREATE TABLE IF NOT EXISTS `matrix_upcoming_news` (
+            `news_id` CHAR(32) NOT NULL COLLATE latin1_bin,
+            `range_id` CHAR(32) NOT NULL COLLATE latin1_bin,
+            `publish_at` INT,
+            `posted` TINYINT(1) NOT NULL DEFAULT 0,
+            `mkdate` INT NOT NULL,
+            `chdate` INT NOT NULL,
+            PRIMARY KEY (`news_id`, `range_id`)
+        ) ENGINE InnoDB ROW_FORMAT=DYNAMIC");
+
+        MatrixPostNewsCronjob::register()->schedulePeriodic(-15)->activate();
+    }
+
+    public function down()
+    {
+        MatrixPostNewsCronjob::unregister();
+
+        DBManager::get()->execute("DROP TABLE IF EXISTS `matrix_upcoming_news`");
+    }
+
+}
\ No newline at end of file
diff --git a/models/MatrixRoom.php b/models/MatrixRoom.php
index 14386e1..5494dea 100644
--- a/models/MatrixRoom.php
+++ b/models/MatrixRoom.php
@@ -124,7 +124,7 @@ class MatrixRoom extends SimpleORMap
 
     public function getLinkedRoom()
     {
-        return new Patrix\Room($this->matrix_room_id, Context::getHeaderLine());
+        return new Patrix\Room($this->matrix_room_id, Context::get() ? Context::getHeaderLine() : '');
     }
 
     public function requireMembership($account)
diff --git a/plugin.manifest b/plugin.manifest
index 77a70c2..a3622a7 100644
--- a/plugin.manifest
+++ b/plugin.manifest
@@ -1,7 +1,7 @@
 pluginname=Matrix-Chat
 pluginclassname=MatrixPlugin
 origin=data-quest
-version=1.2.1
+version=1.3
 screenshot=assets/images/matrix_logo.png
 description=Matrix chat for Stud.IP courses
 studipMinVersion=4.5
-- 
GitLab