aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Digitigrade/Model/Actor.php44
-rw-r--r--Digitigrade/Notification/AdminEditedNoteNotif.php9
-rw-r--r--Digitigrade/Notification/PokeNotif.php62
-rw-r--r--Digitigrade/PokeVerb.php14
-rw-r--r--locale/en_GB.json29
-rw-r--r--misc/send_request_authenticated.php5
-rw-r--r--routes/actor.php19
-rw-r--r--routes/actor_profile_fragment.php14
-rw-r--r--routes/note_fragment.php5
-rw-r--r--routes/preferences.php1
-rw-r--r--static/form.css27
-rw-r--r--templates/actor/profile.php8
-rw-r--r--templates/actor/profile_poke_form.php24
-rw-r--r--templates/preferences_form.php7
14 files changed, 247 insertions, 21 deletions
diff --git a/Digitigrade/Model/Actor.php b/Digitigrade/Model/Actor.php
index 0541e1b..18aba15 100644
--- a/Digitigrade/Model/Actor.php
+++ b/Digitigrade/Model/Actor.php
@@ -3,12 +3,16 @@ namespace Digitigrade\Model;
use Digitigrade\Db;
use Digitigrade\Notification\PendingFollowActionedNotif;
+use Digitigrade\Notification\PokeNotif;
use Digitigrade\Notification\UnblockNotif;
use Digitigrade\Notification\UnfollowNotif;
+use Digitigrade\PokeVerb;
use Digitigrade\RpcException;
use Digitigrade\RpcReceiver;
class Actor extends PushableModel implements RpcReceiver {
+ public const EXTENSION_POKE = 'https://pawpub.entities.org.uk/extension/poke';
+
public ?int $id;
public string $uri;
public bool $isLocal = false;
@@ -135,15 +139,15 @@ class Actor extends PushableModel implements RpcReceiver {
return self::findByUri($uri);
}
- public function rpcCall(string $method, array $args) {
- // $args = [Actor $actingAs]
+ public function rpcCall(string $method, array $args, string $customEndpoint = null) {
+ // $args = [Actor $actingAs, ?string $requestBody]
if (!$this->isLocal) {
// trigger the same action on the remote server
- $endpoint = $this->endpoints->{$method} ?? null;
+ $endpoint = $customEndpoint ?? $this->endpoints->{$method} ?? null;
if (!isset($endpoint)) {
throw new RpcException("Actor `$this->uri` has no $method endpoint");
}
- $result = send_request_authenticated($endpoint, method: 'POST', actingAs: $args[0]);
+ $result = send_request_authenticated($endpoint, method: 'POST', actingAs: $args[0], body: $args[1] ?? null);
if (!str_contains($result->headers[0], ' 20')) {
throw new RpcException('remote call did not indicate success: ' . $result->headers[0]);
}
@@ -188,6 +192,16 @@ class Actor extends PushableModel implements RpcReceiver {
(new UnblockNotif($args[0], $this))->processNotifications();
return;
+ case 'poke':
+ $obj = json_decode($args[1] ?? '');
+ (new PokeNotif(
+ $args[0],
+ PokeVerb::tryFrom($obj?->verb ?? '') ?? PokeVerb::POKE,
+ $this,
+ $obj?->urgent ?? false
+ ))->processNotifications();
+ return;
+
default:
throw new \RuntimeException("behaviour for actor rpc method $method not (yet?) implemented");
}
@@ -329,6 +343,25 @@ class Actor extends PushableModel implements RpcReceiver {
return '/@/' . $this->getFullHandle();
}
+ private function getPokeEndpoint(): ?string {
+ return $this->extensions[self::EXTENSION_POKE] ?? null;
+ }
+
+ public function poke(self $target, PokeVerb $verb = PokeVerb::POKE, bool $urgent = false) {
+ $target->rpcCall('poke', [$this, json_encode([
+ 'verb' => $verb->value,
+ 'urgent' => $urgent
+ ])], $target->getPokeEndpoint());
+ }
+
+ public function isPokeable(): bool {
+ return isset($this->extensions[self::EXTENSION_POKE]);
+ }
+
+ public function setPokeable(bool $pokeable) {
+ $this->extensions[self::EXTENSION_POKE] = $pokeable ? path_to_uri("/actor/$this->id/poke") : null;
+ }
+
public function jsonSerialize(): array {
if ($this->deleted) {
return [
@@ -359,7 +392,8 @@ class Actor extends PushableModel implements RpcReceiver {
'rejectedFollow' => path_to_uri("/actor/$this->id/rejectedFollow"),
'block' => path_to_uri("/actor/$this->id/block"),
'unblock' => path_to_uri("/actor/$this->id/unblock"),
- ]
+ ],
+ 'extensions' => $this->extensions
];
}
}
diff --git a/Digitigrade/Notification/AdminEditedNoteNotif.php b/Digitigrade/Notification/AdminEditedNoteNotif.php
index f3c18a1..888130b 100644
--- a/Digitigrade/Notification/AdminEditedNoteNotif.php
+++ b/Digitigrade/Notification/AdminEditedNoteNotif.php
@@ -2,6 +2,7 @@
namespace Digitigrade\Notification;
use Digitigrade\Model\Note;
+use Digitigrade\Model\Notification;
use Digitigrade\Model\UserAccount;
class AdminEditedNoteNotif implements Notifyable {
@@ -22,7 +23,7 @@ class AdminEditedNoteNotif implements Notifyable {
}
public function getNotificationTitle(): string {
- return __f('notification.adminEditedNote', $this->admin->actor->displayName);
+ return __f('notifications.adminEditedNote', $this->admin->actor->displayName);
}
public function getNotificationTitleLink(): ?string {
@@ -40,4 +41,10 @@ class AdminEditedNoteNotif implements Notifyable {
public function getNotificationImageLink(): ?string {
return $this->admin->actor->getLocalUiHref();
}
+
+ public function processNotifications() {
+ if (!$this->note->author->isLocal)
+ return;
+ Notification::fromNotifyable($this, UserAccount::findByLinkedActor($this->note->author))->send();
+ }
} \ No newline at end of file
diff --git a/Digitigrade/Notification/PokeNotif.php b/Digitigrade/Notification/PokeNotif.php
new file mode 100644
index 0000000..5446098
--- /dev/null
+++ b/Digitigrade/Notification/PokeNotif.php
@@ -0,0 +1,62 @@
+<?php
+namespace Digitigrade\Notification;
+
+use Digitigrade\Model\Actor;
+use Digitigrade\Model\Notification;
+use Digitigrade\Model\UserAccount;
+use Digitigrade\PokeVerb;
+
+class PokeNotif implements Notifyable {
+ private Actor $subject;
+ private PokeVerb $verb;
+ private Actor $object;
+ private bool $urgent;
+
+ public function __construct(Actor $subject, PokeVerb $verb, Actor $object, bool $urgent) {
+ $this->subject = $subject;
+ $this->verb = $verb;
+ $this->object = $object;
+ $this->urgent = $urgent;
+ }
+
+ public function toJsonReference(): mixed {
+ return [$this->subject->id, $this->verb->value, $this->object->id, $this->urgent];
+ }
+
+ public static function fromJsonReference(mixed $reference): ?self {
+ return new self(
+ Actor::find($reference[0]),
+ PokeVerb::from($reference[1]),
+ Actor::find($reference[2]),
+ $reference[3]
+ );
+ }
+
+ public function getNotificationTitle(): string {
+ $kind = $this->urgent ? 'urgent' : 'normal';
+ $verb = $this->verb->value;
+ return __f("notifications.poke.$kind", $this->subject->displayName, __("notifications.poke.verb.$verb"));
+ }
+
+ public function getNotificationTitleLink(): ?string {
+ return $this->subject->getLocalUiHref();
+ }
+
+ public function getNotificationBody(): ?string {
+ return null;
+ }
+
+ public function getNotificationImageUrl(): ?string {
+ return $this->subject->avatar ?? '/static/default-avatar.png';
+ }
+
+ public function getNotificationImageLink(): ?string {
+ return $this->subject->getLocalUiHref();
+ }
+
+ public function processNotifications() {
+ if (!$this->object->isLocal)
+ return;
+ Notification::fromNotifyable($this, UserAccount::findByLinkedActor($this->object))->send();
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/PokeVerb.php b/Digitigrade/PokeVerb.php
new file mode 100644
index 0000000..6490b78
--- /dev/null
+++ b/Digitigrade/PokeVerb.php
@@ -0,0 +1,14 @@
+<?php
+namespace Digitigrade;
+
+enum PokeVerb: string {
+ case POKE = 'poke';
+ case BITE = 'bite';
+ case PUNCH = 'punch';
+ case KICK = 'kick';
+ case WAVE = 'wave';
+ case YELL = 'yell';
+ case HUG = 'hug';
+ case KISS = 'kiss';
+ case PET = 'pet';
+} \ No newline at end of file
diff --git a/locale/en_GB.json b/locale/en_GB.json
index f430064..001aad6 100644
--- a/locale/en_GB.json
+++ b/locale/en_GB.json
@@ -24,6 +24,20 @@
"user.profile.notesHidden": "Notes hidden because you block this user",
"user.profile.feed.rss": "RSS feed",
"user.profile.feed.atom": "Atom feed",
+ "user.profile.poke": "Get this user's attention",
+ "user.profile.poke.verb.label": "Verb",
+ "user.profile.poke.verb.poke": "Poke",
+ "user.profile.poke.verb.bite": "Bite",
+ "user.profile.poke.verb.punch": "Punch",
+ "user.profile.poke.verb.kick": "Kick",
+ "user.profile.poke.verb.wave": "Wave at",
+ "user.profile.poke.verb.yell": "Yell at",
+ "user.profile.poke.verb.hug": "Hug",
+ "user.profile.poke.verb.kiss": "Kiss",
+ "user.profile.poke.verb.pet": "Pet",
+ "user.profile.poke.urgent.label": "Urgent",
+ "user.profile.poke.action": "Send",
+ "user.profile.poke.success": "Sent!",
"user.notes.placeholder": "This user hasn't posted anything yet.",
"user.rss.description": "Public notes by %s",
"timeline.global": "Global timeline",
@@ -85,7 +99,18 @@
"notifications.follow.reject": "%s rejected your follow request",
"notifications.block": "%s blocked you",
"notifications.unblock": "%s unblocked you",
- "notification.adminEditedNote": "An admin (%s) edited your note",
+ "notifications.adminEditedNote": "An admin (%s) edited your note",
+ "notifications.poke.normal": "%s %s you",
+ "notifications.poke.urgent": "%s %s you urgently",
+ "notifications.poke.verb.poke": "poked",
+ "notifications.poke.verb.bite": "bit",
+ "notifications.poke.verb.punch": "punched",
+ "notifications.poke.verb.kick": "kicked",
+ "notifications.poke.verb.wave": "waved at",
+ "notifications.poke.verb.yell": "yelled at",
+ "notifications.poke.verb.hug": "hugged",
+ "notifications.poke.verb.kiss": "kissed",
+ "notifications.poke.verb.pet": "petted",
"login.pageTitle": "Log in",
"login.email": "Email address",
"login.password": "Password",
@@ -125,6 +150,8 @@
"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",
+ "preferences.pokeable.name": "Allow others to poke you",
+ "preferences.pokeable.description": "Also includes other verbs such as bite, wave at, hug, etc.",
"changePassword.heading": "Change password",
"changePassword.currentPassword": "Current password",
"changePassword.newPassword": "New password",
diff --git a/misc/send_request_authenticated.php b/misc/send_request_authenticated.php
index 9019da8..6e74cac 100644
--- a/misc/send_request_authenticated.php
+++ b/misc/send_request_authenticated.php
@@ -16,7 +16,8 @@ function send_request_authenticated(
string $uri,
bool $dontLookUpInstance = false,
string $method = 'GET',
- ?Actor $actingAs = null
+ ?Actor $actingAs = null,
+ ?string $body = null
) {
if (!str_starts_with($uri, 'https://')) {
throw new RuntimeException('refusing to fetch a non-https uri');
@@ -34,7 +35,7 @@ function send_request_authenticated(
if (!isset($instance->auth->outboundToken) || $instance->auth->outboundToken == null) {
// hopefully we can make the request anyway?
- $context = stream_context_create(['http' => ['method' => $method]]);
+ $context = stream_context_create(['http' => ['method' => $method, 'content' => $body]]);
$resp = file_get_contents($uri);
// $http_response_header just poofs into existence .
// i don't like this api. i should probably use curl instead. hmm
diff --git a/routes/actor.php b/routes/actor.php
index d0bb066..a459640 100644
--- a/routes/actor.php
+++ b/routes/actor.php
@@ -1,5 +1,6 @@
<?php
+use Digitigrade\HttpResponseStatus\Forbidden;
use Digitigrade\HttpResponseStatus\NotFound;
use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\HttpResponseStatus\TemporaryRedirect;
@@ -9,6 +10,7 @@ use Digitigrade\Model\FollowRelationStatus;
use Digitigrade\Model\Instance;
use Digitigrade\Model\Note;
use Digitigrade\Model\NotePrivacyScope;
+use Digitigrade\PokeVerb;
use Digitigrade\PolicyManager;
use Digitigrade\Router;
@@ -147,4 +149,21 @@ Router::getInstance()->mount('/actor/:id/unblock', function (array $args) {
throw new Unauthorized('please authenticate yourself on behalf of the initiating actor');
}
$initiator->unblock($target);
+});
+
+Router::getInstance()->mount('/actor/:id/poke', function (array $args) {
+ $target = Actor::find($args['id']);
+ if ($target == null || !$target->isLocal || $target->deleted) {
+ throw new NotFound();
+ }
+ if (!$target->isPokeable()) {
+ throw new Forbidden('you may not poke this user!');
+ }
+ $initiator = Actor::findByRequestHeaders();
+ if ($initiator == null) {
+ throw new Unauthorized('please authenticate yourself on behalf of the initiating actor');
+ }
+ $body = file_get_contents('php://input');
+ $obj = json_decode($body);
+ $initiator->poke($target, PokeVerb::tryFrom($obj?->verb ?? '') ?? PokeVerb::POKE, $obj?->urgent ?? false);
}); \ No newline at end of file
diff --git a/routes/actor_profile_fragment.php b/routes/actor_profile_fragment.php
index d189f87..65f7a49 100644
--- a/routes/actor_profile_fragment.php
+++ b/routes/actor_profile_fragment.php
@@ -1,9 +1,11 @@
<?php
+use Digitigrade\HttpResponseStatus\Forbidden;
use Digitigrade\HttpResponseStatus\PolicyRejected;
use Digitigrade\Model\Actor;
use Digitigrade\Model\StorageObject;
use Digitigrade\Model\UserAccount;
+use Digitigrade\PokeVerb;
use Digitigrade\PolicyManager;
use Digitigrade\Router;
use Digitigrade\StorageProvider\FilesystemStorage;
@@ -62,6 +64,18 @@ Router::getInstance()->mount('/fragment/actor/:id/blockButton', function (array
render_template('actor/profile_block_button', ['user' => $user, 'actor' => $actor]);
});
+Router::getInstance()->mount('/fragment/actor/:id/poke', function (array $args) {
+ $user = UserAccount::requireByCurrentSession();
+ $actor = Actor::find($args['id']);
+
+ if (!$actor->isPokeable()) {
+ throw new Forbidden("can't poke that user");
+ }
+ $user->actor->poke($actor, PokeVerb::from($_POST['verb']), isset($_POST['urgent']));
+
+ render_template('actor/profile_poke_form', ['actor' => $actor, 'poked' => true]);
+});
+
Router::getInstance()->mount('/fragment/profile', function (array $args) {
$user = UserAccount::requireByCurrentSession();
$actor = $user->actor;
diff --git a/routes/note_fragment.php b/routes/note_fragment.php
index 907260d..3d30a7a 100644
--- a/routes/note_fragment.php
+++ b/routes/note_fragment.php
@@ -142,10 +142,7 @@ Router::getInstance()->mount('/fragment/note/:id/edit', function (array $args) {
$note->publish();
if ($user->actor != $note->author) {
- Notification::fromNotifyable(
- new AdminEditedNoteNotif($user, $note),
- UserAccount::findByLinkedActor($note->author)
- )->send();
+ (new AdminEditedNoteNotif($user, $note))->processNotifications();
}
render_template('note/note', ['note' => $note]);
diff --git a/routes/preferences.php b/routes/preferences.php
index 99b8458..95e7228 100644
--- a/routes/preferences.php
+++ b/routes/preferences.php
@@ -16,6 +16,7 @@ Router::getInstance()->mount('/fragment/preferences', function (array $args) {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// updating the preferences
$user->actor->requestToFollow = $_POST['requestToFollow'] == 'true';
+ $user->actor->setPokeable($_POST['pokeable'] == 'true');
$user->actor->save();
$settings->set('interface.smallUnreadIndicators', $_POST['smallIndicators']);
$settings->set('locale.timezone', $_POST['timezone']);
diff --git a/static/form.css b/static/form.css
index bb8655d..39f25cf 100644
--- a/static/form.css
+++ b/static/form.css
@@ -1,19 +1,23 @@
form:not(.nopanel) {
- border: var(--border);
- background: var(--clr-panel-background);
- border-radius: var(--border-radius);
- margin: var(--spacing-double);
- padding: var(--spacing-double);
display: grid;
grid-auto-flow: row;
gap: var(--spacing-single);
max-width: max-content;
+ &:not(.nobackground) {
+ border: var(--border);
+ background: var(--clr-panel-background);
+ border-radius: var(--border-radius);
+ margin: var(--spacing-double);
+ padding: var(--spacing-double);
+ }
+
.row:has(> .column) {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: var(--spacing-single);
+ align-items: baseline;
}
label:has(+ input, + textarea, + select),
@@ -156,6 +160,19 @@ form:not(.nopanel) {
}
}
+ &.pokeForm {
+ .column {
+ display: flex;
+ align-items: center;
+ }
+ label {
+ display: inline;
+ font-size: inherit;
+ color: inherit;
+ margin-right: var(--spacing-single);
+ }
+ }
+
&.settings {
padding: 0;
gap: 0;
diff --git a/templates/actor/profile.php b/templates/actor/profile.php
index be27c86..1e8e51c 100644
--- a/templates/actor/profile.php
+++ b/templates/actor/profile.php
@@ -1,4 +1,6 @@
<?php
+/** @var \Digitigrade\Model\Actor $actor */
+
use Digitigrade\Model\UserAccount;
use Digitigrade\UserSettings;
@@ -72,6 +74,12 @@ $actorUser = UserAccount::findByLinkedActor($actor);
<?php endif; ?>
<?php call_template('actor/profile_follow_button', ['actor' => $actor, 'user' => $user]); ?>
</div>
+ <?php if ($actor->isPokeable()): ?>
+ <details>
+ <summary><?= __('user.profile.poke') ?></summary>
+ <?php call_template('actor/profile_poke_form', ['actor' => $actor]); ?>
+ </details>
+ <?php endif; ?>
<?php elseif ($actor->id == $user->actor->id): ?>
<div class="profileActions">
<button class="primary" hx-get="/fragment/profile" hx-target="closest .fullProfile" hx-swap="outerHTML">
diff --git a/templates/actor/profile_poke_form.php b/templates/actor/profile_poke_form.php
new file mode 100644
index 0000000..7dcc360
--- /dev/null
+++ b/templates/actor/profile_poke_form.php
@@ -0,0 +1,24 @@
+<?php /** @var \Digitigrade\Model\Actor $actor */ ?>
+<form class="nobackground pokeForm" hx-post="/fragment/actor/<?= $actor->id ?>/poke" hx-disabled-elt="find button">
+ <div class="row">
+ <div class="column">
+ <select id="pokeVerb-<?= $actor->id ?>" name="verb">
+ <?php foreach (['poke', 'bite', 'punch', 'kick', 'wave', 'yell', 'hug', 'kiss', 'pet'] as $verb): ?>
+ <option value="<?= $verb ?>"><?= __("user.profile.poke.verb.$verb") ?></option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ <div class="column">
+ <label for="pokeUrgent-<?= $actor->id ?>"><?= __('user.profile.poke.urgent.label') ?></label>
+ <input type="checkbox" id="pokeUrgent-<?= $actor->id ?>" name="urgent">
+ </div>
+ <div class="column">
+ <button type="submit" class="primary"><?= __('user.profile.poke.action') ?></button>
+ </div>
+ <div class="column">
+ <?php if ($poked ?? false): ?>
+ <span class="temporaryIndicator"><?= __('user.profile.poke.success') ?></span>
+ <?php endif; ?>
+ </div>
+ </div>
+</form> \ No newline at end of file
diff --git a/templates/preferences_form.php b/templates/preferences_form.php
index d1694eb..afd487b 100644
--- a/templates/preferences_form.php
+++ b/templates/preferences_form.php
@@ -15,14 +15,15 @@ function _user_pref(string $id, mixed $value, string $type) {
}
?>
-<form class="settings" method="post" action="/todo" hx-post="/fragment/preferences" hx-disabled-elt="find button"
- hx-swap="outerHTML">
+<form class="settings" method="post" action="/todo" hx-post="/fragment/preferences"
+ hx-disabled-elt="find <button[type='submit']/>" hx-swap="outerHTML">
<?php
_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');
+ _user_pref('pokeable', $user->actor->isPokeable(), 'boolean');
?>
<div class="row">
@@ -31,7 +32,7 @@ function _user_pref(string $id, mixed $value, string $type) {
<span class="temporaryIndicator"><?= __('form.saved') ?></span>
<?php endif; ?>
</div>
- <button class="primary"><?= __('form.saveChanges') ?></button>
+ <button class="primary" type="submit"><?= __('form.saveChanges') ?></button>
</div>
</form> \ No newline at end of file