Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ A **commit** is a post on /dev/null, not a Git commit in a repository. Open the

Commits, replies, quotes, scheduling, terminal submissions, and app (signed API) commits share a limit of 10 attempts per minute per user. Posts go through [moderation](#moderation), so a newly submitted commit may be pending rather than public immediately.

Commit cards show where a post was submitted when that source is known: the website, iOS, Android, macOS, the terminal, or an app whose platform is unknown. The label comes from the posting surface, not a value supplied in the post. Older posts and posts scheduled before source tracking was added may have no label, even when submitted later. New posts from apps without a recognized platform are labeled “via App.” The source is included in the author's data export and cleared if account deletion leaves the post as a tombstone.

Approved top-level commits about technology, current affairs or substantive real-world events can receive brief, shared subject headlines. Technical and developer topics carry the most weight. /dev/null community activity also qualifies, including livestreams, Tom, app betas and feature updates. Quote commits include the directly quoted post's text when it is approved and still available, so a brief reaction can share the subject of the post it quotes. Replies, casual small talk and evergreen lifestyle or hobby advice are skipped. OpenAI's Decisions API assesses this text for suitability and checks the closest existing headline names, shortlisted by semantic similarity; when none match, DeepSeek through OpenCode generates a headline from the same text. Posts can be processed together, and similar newly generated headlines share the first headline rather than creating duplicates. No account metadata is sent. A headline expires after fourteen days without a new commit link. This categorization does not change the original post and does not yet add visible headline labels to the interface.

Sources: [composer](resources/js/components/commit-composer.tsx), [commit model](app/Models/Commit.php), [commit validation](app/Http/Requests/CreateCommitRequest.php), [text normalization](app/Services/CommitBodies.php), [submission throttle](app/Http/Middleware/ThrottleCommits.php).
Expand Down
6 changes: 4 additions & 2 deletions app/Actions/CreateCommit.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Dto\NewPoll;
use App\Dto\ThreadEngagement;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Jobs\CheckUserLevel;
use App\Jobs\EmbedCommit;
Expand Down Expand Up @@ -55,15 +56,15 @@ public function __construct(
* @param array<int, string|null> $imageDescriptions
* @param array<string, string> $replyRecipients
*/
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null, array $replyRecipients = [], ?Organization $organization = null): Commit
public function handle(User $user, string $body, ?Commit $parent = null, ?Commit $quotedCommit = null, array $images = [], array $imageDescriptions = [], bool|string $linkPreview = true, ?NewPoll $poll = null, array $replyRecipients = [], ?Organization $organization = null, ?CommitSource $source = null): Commit
{
$url = match ($linkPreview) {
true => $this->urls->first($body),
false => null,
default => $linkPreview,
};

return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll, $replyRecipients, $organization): Commit {
return DB::transaction(function () use ($user, $body, $parent, $quotedCommit, $images, $imageDescriptions, $url, $poll, $replyRecipients, $organization, $source): Commit {
$linkPreview = $this->linkPreviewFor($url);
$createdAt = now();
$hasGoodReputation = $user->hasGoodReputation();
Expand All @@ -72,6 +73,7 @@ public function handle(User $user, string $body, ?Commit $parent = null, ?Commit
$commit = Commit::query()->create([
'user_id' => $user->id,
'author_id' => $organization->id ?? $user->id,
'source' => $source,
'body' => $this->emojiShortcodes->replace($body),
'mentions' => [...$replyRecipients, ...$mentions],
'parent_id' => $parent?->id,
Expand Down
6 changes: 4 additions & 2 deletions app/Actions/CreateExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
public function handle(User $user): array
{
return [
'schema_version' => 5,
'schema_version' => 6,
'exported_at' => now()->toIso8601String(),
'profile' => [
'github_id' => $user->github_id,
Expand Down Expand Up @@ -189,7 +189,7 @@ private function commits(User $user): Generator
{
$commits = $user->commits()
->withTrashed()
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'status', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->select(['id', 'parent_id', 'quoted_commit_id', 'body', 'status', 'source', 'pasteis_count', 'created_at', 'updated_at', 'deleted_at'])
->with('images:id,commit_id')
->lazyById(500);

Expand All @@ -206,6 +206,7 @@ private function commits(User $user): Generator
'body' => $commit->body,
'cached_translations' => $cached[$translations->key($commit)],
'status' => $commit->status->value,
'source' => $commit->source?->value,
'pasteis_count' => $commit->pasteis_count,
'images' => $commit->images->map(fn (CommitImage $image): string => $image->url())->all(),
'created_at' => $commit->created_at->toIso8601String(),
Expand Down Expand Up @@ -239,6 +240,7 @@ private function organizationCommits(User $user): Generator
'quoted_commit_id' => $commit->quoted_commit_id,
'body' => $commit->body,
'status' => $commit->status->value,
'source' => $commit->source?->value,
'images' => $commit->images->map(fn (CommitImage $image): string => $image->url())->all(),
'created_at' => $commit->created_at->toIso8601String(),
'deleted_at' => $commit->deleted_at?->toIso8601String(),
Expand Down
4 changes: 3 additions & 1 deletion app/Actions/CreateScheduledCommit.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Actions;

use App\Dto\NewPoll;
use App\Enums\CommitSource;
use App\Models\Organization;
use App\Models\ScheduledCommit;
use App\Models\User;
Expand All @@ -27,13 +28,14 @@ public function __construct(private CreateImageFiles $createImageFiles, private
* @param list<UploadedFile> $images
* @param array<int, string|null> $imageDescriptions
*/
public function handle(User $user, string $body, CarbonImmutable $scheduledAt, array $images = [], array $imageDescriptions = [], ?NewPoll $poll = null, bool|string $linkPreview = true, ?Organization $organization = null): ScheduledCommit
public function handle(User $user, string $body, CarbonImmutable $scheduledAt, array $images = [], array $imageDescriptions = [], ?NewPoll $poll = null, bool|string $linkPreview = true, ?Organization $organization = null, ?CommitSource $source = null): ScheduledCommit
{
$body = mb_trim($body);

return DB::transaction(fn (): ScheduledCommit => ScheduledCommit::query()->create([
'user_id' => $user->id,
'author_id' => $organization?->id,
'source' => $source,
'body' => $body,
'poll' => $poll instanceof NewPoll ? ['options' => $poll->options, 'duration' => $poll->duration->value] : null,
'images' => $images === [] ? null : $this->createImageFiles->handle($images, $imageDescriptions),
Expand Down
4 changes: 2 additions & 2 deletions app/Actions/DeleteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public function handle(User $user): void
}

$user->commits()->delete();
$user->commits()->withTrashed()->update(['user_id' => null, 'author_id' => null, 'body' => '', 'mentions' => null, 'embedding' => null, 'link_preview_id' => null, 'headlines_classified_at' => null]);
$user->commits()->withTrashed()->update(['user_id' => null, 'author_id' => null, 'body' => '', 'mentions' => null, 'embedding' => null, 'link_preview_id' => null, 'headlines_classified_at' => null, 'source' => null]);

// What they posted for organizations is the organizations' own, so it stays, without who posted it.
Commit::query()->withTrashed()->where('user_id', $user->id)->update(['user_id' => null]);
Commit::query()->withTrashed()->where('user_id', $user->id)->update(['user_id' => null, 'source' => null]);

$this->deleteOrgs->handle($this->emptyOrgs($user));

Expand Down
2 changes: 1 addition & 1 deletion app/Actions/UpdateScheduledCommitSubmittedAt.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function handle(string $id, bool $early = false): bool
$scheduled = ScheduledCommit::query()->with(['user', 'author'])->findOrFail($id);
assert(is_string($scheduled->body));
$poll = $scheduled->poll === null ? null : new NewPoll($scheduled->poll['options'], PollDuration::from($scheduled->poll['duration']));
$commit = $this->createCommit->handle($scheduled->user, $scheduled->body, linkPreview: $scheduled->link_preview_url ?? false, poll: $poll, organization: $scheduled->author?->organization);
$commit = $this->createCommit->handle($scheduled->user, $scheduled->body, linkPreview: $scheduled->link_preview_url ?? false, poll: $poll, organization: $scheduled->author?->organization, source: $scheduled->source);
$commit->images()->createMany($scheduled->images ?? []);

$scheduled->forceFill(['commit_id' => $commit->id, 'body' => null, 'poll' => null, 'images' => null, 'link_preview_url' => null])->save();
Expand Down
33 changes: 33 additions & 0 deletions app/Enums/CommitSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace App\Enums;

/**
* Where a commit was created, not the operating system of a web browser.
* Native apps identify themselves when the user approves their device.
*/
enum CommitSource: string
{
case Web = 'web';
case Ios = 'ios';
case Android = 'android';
case Macos = 'macos';
case Cli = 'cli';
case Api = 'api';

public static function fromAppId(?string $appId): self
{
if ($appId === null || ! str_contains($appId, ':')) {
return self::Api;
}

return match (explode(':', $appId, 2)[0]) {
'ios' => self::Ios,
'android' => self::Android,
'macos' => self::Macos,
default => self::Api,
};
}
}
7 changes: 6 additions & 1 deletion app/Http/Controllers/Api/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Enums\CommitSource;
use App\Http\Requests\CreateApiCommitRequest;
use App\Http\Requests\DeleteApiCommitRequest;
use App\Http\Requests\ShowApiCommitRequest;
use App\Http\Resources\CommitResource;
use App\Models\Commit;
use App\Models\Device;
use App\Models\User;
use App\Queries\CommitAncestorsQuery;
use App\Queries\CommitPageQuery;
Expand Down Expand Up @@ -51,7 +53,10 @@ public function show(ShowApiCommitRequest $request, #[CurrentUser] ?User $curren
*/
public function store(CreateApiCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action, CommitPageQuery $page): JsonResponse
{
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll(), replyRecipients: $request->replyRecipients(), organization: $request->organization());
$device = $request->attributes->get('authenticated_device');
$source = CommitSource::fromAppId($device instanceof Device ? $device->app_id : null);

$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), $request->quotedCommit(), $request->images(), $request->imageDescriptions(), poll: $request->poll(), replyRecipients: $request->replyRecipients(), organization: $request->organization(), source: $source);

return CommitResource::make($page->get($commit, $user))
->response()
Expand Down
7 changes: 6 additions & 1 deletion app/Http/Controllers/Api/ScheduledCommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use App\Actions\CreateScheduledCommit;
use App\Actions\DeleteScheduledCommit;
use App\Enums\CommitSource;
use App\Http\Requests\CreateApiScheduledCommitRequest;
use App\Http\Requests\DeleteApiScheduledCommitRequest;
use App\Http\Resources\ScheduledCommitResource;
use App\Models\Device;
use App\Models\ScheduledCommit;
use App\Models\User;
use App\Queries\ScheduledCommitsQuery;
Expand All @@ -33,7 +35,10 @@ public function index(#[CurrentUser] User $user, ScheduledCommitsQuery $schedule
*/
public function store(CreateApiScheduledCommitRequest $request, #[CurrentUser] User $user, CreateScheduledCommit $action): JsonResponse
{
$scheduled = $action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization());
$device = $request->attributes->get('authenticated_device');
$source = CommitSource::fromAppId($device instanceof Device ? $device->app_id : null);

$scheduled = $action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization(), source: $source);

return ScheduledCommitResource::make($scheduled)
->response()
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/Cli/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Http\Controllers\Cli;

use App\Actions\CreateCommit;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCliCommitRequest;
use App\Models\User;
Expand All @@ -15,7 +16,7 @@
{
public function store(CreateCliCommitRequest $request, #[CurrentUser] User $user, CreateCommit $action): Response
{
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), images: $request->images());
$commit = $action->handle($user, $request->string('body')->value(), $request->parent(), images: $request->images(), source: CommitSource::Cli);

$url = route('commit.show', [$user, $commit]);

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/CommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Dto\ThreadContinuation;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitRequest;
use App\Http\Requests\DeleteCommitRequest;
Expand Down Expand Up @@ -68,7 +69,7 @@ public function store(CreateCommitRequest $request, #[CurrentUser] User $user, C
$before = $achievements->unlockedBadgeNames($user);

$organization = $request->organization();
$commit = $action->handle($user, $request->string('body')->value(), quotedCommit: $request->quotedCommit(), images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), poll: $request->poll(), organization: $organization);
$commit = $action->handle($user, $request->string('body')->value(), quotedCommit: $request->quotedCommit(), images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), poll: $request->poll(), organization: $organization, source: CommitSource::Web);

$unlocked = $achievements->unlockedBadgeNames($user)->diff($before)->values();

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/CommitReplyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Actions\CreateCommit;
use App\Actions\DeleteCommit;
use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Http\Requests\CreateCommitReplyRequest;
use App\Http\Requests\DeleteCommitReplyRequest;
Expand All @@ -28,7 +29,7 @@ public function index(ListCommitRepliesRequest $request, #[CurrentUser] ?User $u
public function store(CreateCommitReplyRequest $request, #[CurrentUser] User $user, Commit $commit, CreateCommit $action, CommitThreadQuery $thread): JsonResponse
{
$organization = $request->organization();
$reply = $action->handle($user, $request->string('body')->value(), $commit, images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), replyRecipients: $request->replyRecipients(), organization: $organization);
$reply = $action->handle($user, $request->string('body')->value(), $commit, images: $request->images(), imageDescriptions: $request->imageDescriptions(), linkPreview: $request->linkPreview(), replyRecipients: $request->replyRecipients(), organization: $organization, source: CommitSource::Web);
$parent = $thread->get($commit, $user);

return response()->json([
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/ScheduledCommitController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Actions\CreateScheduledCommit;
use App\Actions\DeleteScheduledCommit;
use App\Enums\CommitSource;
use App\Http\Requests\CreateScheduledCommitRequest;
use App\Http\Requests\DeleteScheduledCommitRequest;
use App\Http\Resources\ScheduledCommitResource;
Expand All @@ -32,7 +33,7 @@ public function index(Request $request, #[CurrentUser] User $user, ScheduledComm

public function store(CreateScheduledCommitRequest $request, #[CurrentUser] User $user, CreateScheduledCommit $action): RedirectResponse
{
$action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization());
$action->handle($user, $request->string('body')->value(), CarbonImmutable::parse($request->string('scheduled_at')->value())->utc(), $request->images(), $request->imageDescriptions(), $request->poll(), $request->linkPreview(), $request->organization(), source: CommitSource::Web);

Inertia::flash('scheduled', true);
Inertia::flash('toast', ['type' => 'success', 'message' => 'Commit scheduled. Only you can see it until it is submitted.']);
Expand Down
1 change: 1 addition & 0 deletions app/Http/Middleware/AuthenticateSignedRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public function handle(Request $request, Closure $next, ?string $mode = null): R
throw_if($device->user->isBanned() && ! $request->routeIs('api-current-user.destroy'), ApiException::banned());

Auth::setUser($device->user);
$request->attributes->set('authenticated_device', $device);

return $next($request);
}
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/CommitResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function toArray(Request $request): array
'parent_id' => $isSigned ? $this->parent_id : $this->parent?->id,
'html_url' => route('commit.show', [$this->author, $this->resource]),
'created_at' => $this->created_at->toIso8601ZuluString(),
...($this->source === null ? [] : ['source' => $this->source->value]),
'stars_count' => $this->whenCounted('stars'),
'replies_count' => $this->whenCounted('replies'),
'link_preview' => $this->whenLoaded('linkPreview'),
Expand Down
3 changes: 3 additions & 0 deletions app/Models/Commit.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Models;

use App\Enums\CommitSource;
use App\Enums\CommitStatus;
use App\Queries\CommitBuilder;
use App\Services\CommitBodies;
Expand Down Expand Up @@ -33,6 +34,7 @@
* @property-read string $body
* @property-read array<string, string>|null $mentions
* @property-read array<string, string>|null $organization_mentions
* @property-read CommitSource|null $source
* @property-read CommitStatus $status
* @property-read string|null $link_preview_id
* @property-read string|null $quoted_commit_id
Expand Down Expand Up @@ -127,6 +129,7 @@ public function casts(): array
'mentions' => 'array',
'organization_mentions' => 'array',
'status' => CommitStatus::class,
'source' => CommitSource::class,
'link_preview_id' => 'string',
'quoted_commit_id' => 'string',
'ai_decision' => CommitStatus::class,
Expand Down
Loading