aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorwinter2025-03-10 20:13:40 +0000
committerwinter2025-03-10 20:13:40 +0000
commit30fa49bc3fa4f94184d693932e838c0672f24a16 (patch)
treed15bccf49c5c15905bb581e2f394b281ce852ee3
parent2bd4a086b0f50b1b88faf846da91267290ca4b07 (diff)
add user rss/atom feeds
-rw-r--r--locale/en_GB.json5
-rw-r--r--misc/__.php4
-rw-r--r--routes/actor_rss.php44
-rw-r--r--routes/preferences.php1
-rw-r--r--settings.user.ini6
-rw-r--r--static/icons.css6
-rw-r--r--templates/actor_atom.php51
-rw-r--r--templates/actor_profile.php23
-rw-r--r--templates/actor_profile_page.php23
-rw-r--r--templates/actor_rss.php46
-rw-r--r--templates/note.php4
-rw-r--r--templates/preferences_form.php1
-rw-r--r--templates/skeleton.php7
13 files changed, 210 insertions, 11 deletions
diff --git a/locale/en_GB.json b/locale/en_GB.json
index 75cf63b..69bf6a1 100644
--- a/locale/en_GB.json
+++ b/locale/en_GB.json
@@ -22,7 +22,10 @@
"user.profile.avatarReset": "Avatar reset!",
"user.profile.bio": "Bio",
"user.profile.notesHidden": "Notes hidden because you block this user",
+ "user.profile.feed.rss": "RSS feed",
+ "user.profile.feed.atom": "Atom feed",
"user.notes.placeholder": "This user hasn't posted anything yet.",
+ "user.rss.description": "Public notes by %s",
"timeline.global": "Global timeline",
"timeline.global.description": "This timeline shows all known public indexable notes.",
"timeline.global.shortName": "Global",
@@ -105,6 +108,8 @@
"preferences.timezone.name": "Time zone",
"preferences.timezone.description": "Preferred time zone, which all dates and times are shown in",
"preferences.timezone.auto": "Set automatically",
+ "preferences.rssEnabled.name": "Enable RSS and Atom feeds",
+ "preferences.rssEnabled.description": "Provides a feed of your public notes, usable with common feed reader apps and online aggregators",
"changePassword.heading": "Change password",
"changePassword.currentPassword": "Current password",
"changePassword.newPassword": "New password",
diff --git a/misc/__.php b/misc/__.php
index af6366a..e0c09ab 100644
--- a/misc/__.php
+++ b/misc/__.php
@@ -4,4 +4,8 @@ use Digitigrade\Text;
function __(string $key): string {
return (new Text($key))->__tostring();
+}
+
+function __f(string $key, mixed ...$args): string {
+ return sprintf(__($key), ...$args);
} \ No newline at end of file
diff --git a/routes/actor_rss.php b/routes/actor_rss.php
new file mode 100644
index 0000000..67d52ad
--- /dev/null
+++ b/routes/actor_rss.php
@@ -0,0 +1,44 @@
+<?php
+
+use Digitigrade\HttpResponseStatus\Forbidden;
+use Digitigrade\HttpResponseStatus\NotFound;
+use Digitigrade\Model\Actor;
+use Digitigrade\Model\UserAccount;
+use Digitigrade\Router;
+use Digitigrade\Text;
+use Digitigrade\UserSettings;
+
+$checkActorFeedEnabled = function (?Actor $actor) {
+ if ($actor == null || $actor->deleted) {
+ throw new NotFound('there is no actor with that id');
+ }
+ if (!$actor->isLocal) {
+ throw new NotFound('refusing to give you an rss feed for a non-local actor go talk to their home instance');
+ }
+ $user = UserAccount::findByLinkedActor($actor);
+ if ($user == null) {
+ throw new NotFound('im truly discombobulated');
+ }
+ $rssEnabled = (new UserSettings($user))->getBool('profile.rssEnabled');
+ if (!$rssEnabled) {
+ throw new Forbidden('this user has not enabled RSS/Atom feeds for their profile');
+ }
+};
+
+Router::getInstance()->mount('/actor/:id/feed/:lang/feed.rss', function (array $args) use ($checkActorFeedEnabled) {
+ $actor = Actor::find($args['id']);
+ $checkActorFeedEnabled($actor);
+ header('Content-Type: application/rss+xml');
+ Text::setDefaultLanguage($args['lang']);
+ render_template('actor_rss', ['actor' => $actor, 'lang' => $args['lang']]);
+});
+
+Router::getInstance()->mount('/actor/:id/feed/:lang/feed.atom', function (array $args) use ($checkActorFeedEnabled) {
+ $actor = Actor::find($args['id']);
+ $checkActorFeedEnabled($actor);
+ header('Content-Type: application/atom+xml');
+ Text::setDefaultLanguage($args['lang']);
+ render_template('actor_atom', ['actor' => $actor, 'lang' => $args['lang']]);
+});
+
+unset($checkActorFeedEnabled);
diff --git a/routes/preferences.php b/routes/preferences.php
index 243f90d..99b8458 100644
--- a/routes/preferences.php
+++ b/routes/preferences.php
@@ -19,6 +19,7 @@ Router::getInstance()->mount('/fragment/preferences', function (array $args) {
$user->actor->save();
$settings->set('interface.smallUnreadIndicators', $_POST['smallIndicators']);
$settings->set('locale.timezone', $_POST['timezone']);
+ $settings->set('profile.rssEnabled', $_POST['rssEnabled']);
$saved = true;
}
diff --git a/settings.user.ini b/settings.user.ini
index 7baa633..8725114 100644
--- a/settings.user.ini
+++ b/settings.user.ini
@@ -7,4 +7,8 @@ type = "boolean"
[locale.timezone]
default = "Europe/London"
-type = "timezone" \ No newline at end of file
+type = "timezone"
+
+[profile.rssEnabled]
+default = "false"
+type = "boolean" \ No newline at end of file
diff --git a/static/icons.css b/static/icons.css
index a48a4f4..3d53416 100644
--- a/static/icons.css
+++ b/static/icons.css
@@ -111,4 +111,10 @@
&.share {
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M17 22q-1.25 0-2.125-.875T14 19q0-.15.075-.7L7.05 14.2q-.4.375-.925.588T5 15q-1.25 0-2.125-.875T2 12t.875-2.125T5 9q.6 0 1.125.213t.925.587l7.025-4.1q-.05-.175-.062-.337T14 5q0-1.25.875-2.125T17 2t2.125.875T20 5t-.875 2.125T17 8q-.6 0-1.125-.213T14.95 7.2l-7.025 4.1q.05.175.063.338T8 12t-.012.363t-.063.337l7.025 4.1q.4-.375.925-.587T17 16q1.25 0 2.125.875T20 19t-.875 2.125T17 22'/%3E%3C/svg%3E");
}
+ &.rss-feed {
+ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M5 21q-.825 0-1.412-.587T3 19t.588-1.412T5 17t1.413.588T7 19t-.587 1.413T5 21m12 0q0-2.925-1.1-5.462t-3-4.438t-4.437-3T3 7V4q3.55 0 6.625 1.325t5.4 3.65t3.65 5.4T20 21zm-6 0q0-1.675-.625-3.113T8.65 15.35t-2.537-1.725T3 13v-3q2.3 0 4.288.863t3.487 2.362t2.363 3.488T14 21z'/%3E%3C/svg%3E");
+ }
+ &.orbit-outline {
+ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M6 21.5q-1.45 0-2.475-1.025T2.5 18t1.025-2.475T6 14.5t2.475 1.025T9.5 18q0 .55-.162 1.063t-.488.962v-.675q.75.325 1.55.488T12 20q3.35 0 5.675-2.325T20 12h2q0 2.075-.788 3.9t-2.137 3.175t-3.175 2.138T12 22q-1.125 0-2.2-.238t-2.075-.712q-.4.225-.837.338T6 21.5m0-2q.625 0 1.063-.437T7.5 18t-.437-1.062T6 16.5t-1.062.438T4.5 18t.438 1.063T6 19.5m6-4q-1.45 0-2.475-1.025T8.5 12t1.025-2.475T12 8.5t2.475 1.025T15.5 12t-1.025 2.475T12 15.5M2 12q0-2.075.788-3.9t2.137-3.175T8.1 2.788T12 2q1.125 0 2.2.238t2.075.712q.4-.225.838-.338T18 2.5q1.45 0 2.475 1.025T21.5 6t-1.025 2.475T18 9.5t-2.475-1.025T14.5 6q0-.55.163-1.062t.487-.963v.675q-.75-.325-1.55-.488T12 4Q8.65 4 6.325 6.325T4 12zm16-4.5q.625 0 1.063-.437T19.5 6t-.437-1.062T18 4.5t-1.062.438T16.5 6t.438 1.063T18 7.5M18 6'/%3E%3C/svg%3E");
+ }
}
diff --git a/templates/actor_atom.php b/templates/actor_atom.php
new file mode 100644
index 0000000..8bd40fb
--- /dev/null
+++ b/templates/actor_atom.php
@@ -0,0 +1,51 @@
+<?php
+/** @var \Digitigrade\Model\Actor $actor */
+/** @var string $lang */
+
+use Digitigrade\GlobalSettings;
+use Digitigrade\Model\Note;
+use Digitigrade\Model\NotePrivacyScope;
+
+$selfUri = path_to_uri("/actor/$actor->id/feed/$lang/feed.atom");
+$actorPage = path_to_uri("/@/$actor->handle");
+
+echo '<?xml version="1.0" encoding="utf-8"?>';
+?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <id><?= $selfUri ?></id>
+ <title>
+ <?= htmlspecialchars($actor->displayName) ?> — <?= GlobalSettings::getInstance()->get('instance.name') ?>
+ </title>
+ <updated><?= Note::findAllWithAuthor($actor, 1)[0]->created->format(DateTime::ATOM) ?></updated>
+ <author>
+ <name><?= htmlspecialchars($actor->displayName) ?></name>
+ <uri><?= $actorPage ?></uri>
+ </author>
+ <link rel="self" href="<?= $selfUri ?>" />
+ <link rel="alternate" type="text/html" href="<?= $actorPage ?>" />
+ <generator version="<?= get_version() ?>">Digitigrade</generator>
+ <?php if (isset($actor->avatar)): ?>
+ <icon><?= $actor->avatar ?></icon>
+ <?php endif; ?>
+ <subtitle><?= __f('user.rss.description', $actor->displayName) ?></subtitle>
+
+ <?php foreach (Note::findAllWithAuthor($actor, limit: 50) as $note): ?>
+ <?php
+ /** @var Note $note */
+ if ($note->privacy->scope != NotePrivacyScope::PUBLIC || $note->inReplyTo != null) {
+ continue;
+ }
+ ?>
+ <entry>
+ <id><?= path_to_uri("/note/$note->id") ?></id>
+ <title><![CDATA[<?= htmlspecialchars(substr($note->plainContent, 0, 50), double_encode: false) ?>]]></title>
+ <updated><?= ($note->modified ?? $note->created)->format(DateTime::ATOM) ?></updated>
+ <link rel="alternate" type="text/html" href="<?= path_to_uri("/@/$actor->handle/note/$note->id") ?>" />
+ <content type="html"><![CDATA[<?= $note->getBestContentAsHtml() ?>]]></content>
+ <?php foreach ($note->attachments as $file): ?>
+ <link rel="enclosure" title="<?= htmlspecialchars($file->description) ?>" type="<?= $file->type ?>"
+ href="<?= $file->href ?>" />
+ <?php endforeach; ?>
+ </entry>
+ <?php endforeach; ?>
+</feed> \ No newline at end of file
diff --git a/templates/actor_profile.php b/templates/actor_profile.php
index ca19978..dfb0174 100644
--- a/templates/actor_profile.php
+++ b/templates/actor_profile.php
@@ -1,7 +1,9 @@
<?php
use Digitigrade\Model\UserAccount;
+use Digitigrade\UserSettings;
$user = UserAccount::findByCurrentSession();
+$actorUser = UserAccount::findByLinkedActor($actor);
?>
<div class="fullProfile">
<div class="basicInfo">
@@ -23,11 +25,7 @@ $user = UserAccount::findByCurrentSession();
</div>
<div class="profileMiniActions">
- <?php call_template('menu_button', [], function () {
- global $actor;
- // i don't know why but `global $user` just straight up doesnt work?
- $user = UserAccount::findByCurrentSession();
- ?>
+ <?php call_template('menu_button', [], function () use ($actor, $user, $actorUser) { ?>
<?php if (!$actor->isLocal && isset($actor->homepage)): ?>
<li>
<a href="<?= $actor->homepage ?>" target="_blank">
@@ -36,6 +34,21 @@ $user = UserAccount::findByCurrentSession();
</a>
</li>
<?php endif; ?>
+ <?php if ($actorUser != null && (new UserSettings($actorUser))->getBool('profile.rssEnabled')): ?>
+ <?php $lang = get_ui_language(); ?>
+ <li>
+ <a href="<?= "/actor/$actor->id/feed/$lang/feed.rss" ?>" target="_blank" type="application/rss+xml">
+ <span class="material-symbols rss-feed"></span>
+ <span><?= __('user.profile.feed.rss') ?></span>
+ </a>
+ </li>
+ <li>
+ <a href="<?= "/actor/$actor->id/feed/$lang/feed.atom" ?>" target="_blank" type="application/atom+xml">
+ <span class="material-symbols orbit-outline"></span>
+ <span><?= __('user.profile.feed.atom') ?></span>
+ </a>
+ </li>
+ <?php endif; ?>
<?php if ($user != null && $user->actor != $actor): ?>
<li><?php call_template('actor_profile_block_button', ['user' => $user, 'actor' => $actor]) ?></li>
<?php endif; ?>
diff --git a/templates/actor_profile_page.php b/templates/actor_profile_page.php
index dbeb86a..1b2981d 100644
--- a/templates/actor_profile_page.php
+++ b/templates/actor_profile_page.php
@@ -2,6 +2,10 @@
use Digitigrade\Model\Note;
use Digitigrade\Model\UserAccount;
+use Digitigrade\UserSettings;
+
+$lang = get_ui_language();
+$actorUser = UserAccount::findByLinkedActor($actor);
call_template('skeleton', [
'pageTitle' => "$actor->displayName - @" . $actor->getFullHandle(),
@@ -10,9 +14,22 @@ call_template('skeleton', [
'ogDesc' => $actor->bio,
'ogImage' => $actor->avatar ?? path_to_uri('/static/default-avatar.png'),
'ogType' => 'profile',
- 'ogHandle' => '@' . $actor->getFullHandle(true)
-], function () {
- global $actor;
+ 'ogHandle' => '@' . $actor->getFullHandle(true),
+ 'headLinks' => ($actorUser != null && (new UserSettings($actorUser))->getBool('profile.rssEnabled')) ? [
+ [
+ 'rel' => 'alternate',
+ 'type' => 'application/rss+xml',
+ 'href' => "/actor/$actor->id/feed/$lang/feed.rss",
+ 'title' => __('user.profile.feed.rss')
+ ],
+ [
+ 'rel' => 'alternate',
+ 'type' => 'application/atom+xml',
+ 'href' => "/actor/$actor->id/feed/$lang/feed.atom",
+ 'title' => __('user.profile.feed.atom')
+ ]
+ ] : []
+], function () use ($actor) {
$user = UserAccount::findByCurrentSession();
call_template('actor_profile', ['actor' => $actor]);
diff --git a/templates/actor_rss.php b/templates/actor_rss.php
new file mode 100644
index 0000000..03fc4f5
--- /dev/null
+++ b/templates/actor_rss.php
@@ -0,0 +1,46 @@
+<?php
+/** @var \Digitigrade\Model\Actor $actor */
+/** @var string $lang */
+
+use Digitigrade\GlobalSettings;
+use Digitigrade\Model\Note;
+use Digitigrade\Model\NotePrivacyScope;
+
+// putting this here because otherwise my ide yells about "shorthand <?"
+// because it thinks i'm trying to do a php thing instead of xml directive
+echo '<?xml version="1.0" encoding="utf-8"?>';
+?>
+<rss version="2.0">
+ <channel>
+ <title>
+ <?= htmlspecialchars($actor->displayName) ?> — <?= GlobalSettings::getInstance()->get('instance.name') ?>
+ </title>
+ <link><?= path_to_uri("/@/$actor->handle") ?></link>
+ <description><?= __f('user.rss.description', htmlspecialchars($actor->displayName)) ?></description>
+ <language><?= $lang ?></language>
+ <generator><?= __f('version', get_version()) ?></generator>
+ <?php if (isset($actor->avatar)): ?>
+ <image>
+ <url><?= $actor->avatar ?></url>
+ </image>
+ <?php endif; ?>
+ <?php foreach (Note::findAllWithAuthor($actor, limit: 50) as $note): ?>
+ <?php
+ /** @var Note $note */
+ if ($note->privacy->scope != NotePrivacyScope::PUBLIC || $note->inReplyTo != null) {
+ continue;
+ }
+ ?>
+ <item>
+ <link><?= path_to_uri("/@/$actor->handle/note/$note->id") ?></link>
+ <guid><?= path_to_uri("/note/$note->id") ?></guid>
+ <title><![CDATA[<?= htmlspecialchars(substr($note->plainContent, 0, 50), double_encode: false) ?>]]></title>
+ <description><![CDATA[<?= $note->getBestContentAsHtml() ?>]]></description>
+ <pubDate><?= $note->created->format(DateTime::RSS) ?></pubDate>
+ <?php foreach ($note->attachments as $file): ?>
+ <enclosure url="<?= $file->href ?>" type="<?= $file->type ?>" size="-1" /><!-- size unknown sorry :( -->
+ <?php endforeach; ?>
+ </item>
+ <?php endforeach; ?>
+ </channel>
+</rss> \ No newline at end of file
diff --git a/templates/note.php b/templates/note.php
index 6835a0f..94edf2b 100644
--- a/templates/note.php
+++ b/templates/note.php
@@ -1,4 +1,6 @@
<?php
+/** @var \Digitigrade\Model\Note $note */
+
use Digitigrade\Model\Actor;
use Digitigrade\Model\InteractionKind;
use Digitigrade\Model\NotePrivacyScope;
@@ -79,7 +81,7 @@ $pathToSelf = '/@/' . $note->author->getFullHandle() . '/note/' . $note->id;
<details>
<summary><?= htmlspecialchars($note->summary) ?></summary>
<?php endif; ?>
- <div class="content">
+ <div class="content" lang="<?= $note->language ?>">
<?= $note->getBestContentAsHtml() ?>
</div>
diff --git a/templates/preferences_form.php b/templates/preferences_form.php
index 33418f6..d1694eb 100644
--- a/templates/preferences_form.php
+++ b/templates/preferences_form.php
@@ -22,6 +22,7 @@ function _user_pref(string $id, mixed $value, string $type) {
_user_pref('requestToFollow', $user->actor->requestToFollow, 'boolean');
_user_pref('smallIndicators', $settings->getBool('interface.smallUnreadIndicators'), 'boolean');
_user_pref('timezone', $settings->get('locale.timezone'), 'timezone');
+ _user_pref('rssEnabled', $settings->getBool('profile.rssEnabled'), 'boolean');
?>
<div class="row">
diff --git a/templates/skeleton.php b/templates/skeleton.php
index 7f40611..5848054 100644
--- a/templates/skeleton.php
+++ b/templates/skeleton.php
@@ -7,7 +7,7 @@ $user = UserAccount::findByCurrentSession();
$settings = GlobalSettings::getInstance();
?>
<!DOCTYPE html>
-<html lang="en">
+<html lang="<?= get_ui_language() ?>">
<head>
<title><?= $pageTitle ?> &mdash; <?= $settings->get('instance.name') ?></title>
@@ -29,6 +29,11 @@ $settings = GlobalSettings::getInstance();
<?php if (isset($ogHandle)): ?>
<meta name="fediverse:creator" content="<?= $ogHandle ?>">
<?php endif; ?>
+ <!-- any custom links e.g. rss/atom feeds -->
+ <?php foreach (($headLinks ?? []) as $link): ?>
+ <link rel="<?= $link['rel'] ?>" type="<?= $link['type'] ?? '' ?>" href="<?= $link['href'] ?>"
+ title="<?= $link['title'] ?? '' ?>">
+ <?php endforeach; ?>
<!-- other page resources -->
<?php call_template('head_tags'); ?>
</head>