diff options
| -rw-r--r-- | locale/en_GB.json | 5 | ||||
| -rw-r--r-- | misc/__.php | 4 | ||||
| -rw-r--r-- | routes/actor_rss.php | 44 | ||||
| -rw-r--r-- | routes/preferences.php | 1 | ||||
| -rw-r--r-- | settings.user.ini | 6 | ||||
| -rw-r--r-- | static/icons.css | 6 | ||||
| -rw-r--r-- | templates/actor_atom.php | 51 | ||||
| -rw-r--r-- | templates/actor_profile.php | 23 | ||||
| -rw-r--r-- | templates/actor_profile_page.php | 23 | ||||
| -rw-r--r-- | templates/actor_rss.php | 46 | ||||
| -rw-r--r-- | templates/note.php | 4 | ||||
| -rw-r--r-- | templates/preferences_form.php | 1 | ||||
| -rw-r--r-- | templates/skeleton.php | 7 |
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 ?> — <?= $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> |
