pl-api: pixelfed stories

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-05-29 14:54:12 +02:00
parent 92be17d0e2
commit 62f65bfe9d
7 changed files with 274 additions and 0 deletions

View File

@ -76,6 +76,9 @@ import {
statusEditSchema,
statusSchema,
statusSourceSchema,
storyCarouselItemSchema,
storyMediaSchema,
storyProfileSchema,
streamingEventSchema,
subscriptionDetailsSchema,
subscriptionInvoiceSchema,
@ -248,6 +251,7 @@ import type {
GetStatusQuotesParams,
GetStatusReferencesParams,
} from './params/statuses';
import type { CreateStoryParams, CreateStoryPollParams, CropStoryPhotoParams, StoryReportType } from './params/stories';
import type {
AntennaTimelineParams,
BubbleTimelineParams,
@ -5767,6 +5771,132 @@ class PlApiClient {
},
};
public readonly stories = {
getRecentStories: async () => {
const response = await this.request('/api/web/stories/v1/recent');
return v.parse(filteredArray(storyCarouselItemSchema), response.json);
},
getStoryViewers: async (storyId: string) => {
const response = await this.request('/api/web/stories/v1/viewers', {
params: { sid: storyId },
});
return v.parse(filteredArray(accountSchema), response.json);
},
getStoriesForProfile: async (accountId: string) => {
const response = await this.request(`/api/web/stories/v1/profile/${accountId}`);
return v.parse(filteredArray(storyProfileSchema), response.json);
},
storyExists: async (accountId: string) => {
const response = await this.request(`/api/web/stories/v1/exists/${accountId}`);
return v.parse(v.boolean(), response.json);
},
getStoryPollResults: async (storyId: string) => {
const response = await this.request('/api/web/stories/v1/poll/results', {
params: { sid: storyId },
});
return v.parse(v.array(v.number()), response.json);
},
markStoryAsViewed: async (storyId: string) => {
const response = await this.request<{}>('/api/web/stories/v1/viewed', {
method: 'POST',
body: { id: storyId },
});
return response.json;
},
createStoryReaction: async (storyId: string, emoji: string) => {
const response = await this.request<{}>('/api/web/stories/v1/react', {
method: 'POST',
body: { sid: storyId, reaction: emoji },
});
return response.json;
},
createStoryComment: async (storyId: string, comment: string) => {
const response = await this.request<{}>('/api/web/stories/v1/comment', {
method: 'POST',
body: { sid: storyId, caption: comment },
});
return response.json;
},
createStoryPoll: async (params: CreateStoryPollParams) => {
const response = await this.request<{}>('/api/web/stories/v1/publish/poll', {
method: 'POST',
body: params,
});
return response.json;
},
storyPollVote: async (storyId: string, choiceId: number) => {
const response = await this.request<{}>('/api/web/stories/v1/publish/poll', {
method: 'POST',
body: { sid: storyId, ci: choiceId },
});
return response.json;
},
reportStory: async (storyId: string, type: StoryReportType) => {
const response = await this.request<{}>('/api/web/stories/v1/report', {
method: 'POST',
body: { id: storyId, type },
});
return response.json;
},
addMedia: async (file: File) => {
const response = await this.request('/api/web/stories/v1/add', {
method: 'POST',
body: { file },
contentType: '',
});
return v.parse(storyMediaSchema, response.json);
},
cropPhoto: async (mediaId: string, params: CropStoryPhotoParams) => {
const response = await this.request<{}>('/api/web/stories/v1/crop', {
method: 'POST',
body: { media_id: mediaId, ...params },
});
return response.json;
},
createStory: async (mediaId: string, params: CreateStoryParams) => {
const response = await this.request<{}>('/api/web/stories/v1/publish', {
method: 'POST',
body: { media_id: mediaId, ...params },
});
return response.json;
},
deleteStory: async (storyId: string) => {
const response = await this.request<{}>(`/api/web/stories/v1/delete/${storyId}`, {
method: 'DELETE',
});
return response.json;
},
};
/** Routes that are not part of any stable release */
public readonly experimental = {
admin: {

View File

@ -76,6 +76,9 @@ export * from './shout-message';
export * from './status';
export * from './status-edit';
export * from './status-source';
export * from './story-carousel-item';
export * from './story-media';
export * from './story-profile';
export * from './streaming-event';
export * from './subscription-details';
export * from './subscription-invoice';

View File

@ -0,0 +1,30 @@
import * as v from 'valibot';
/**
* @category Schemas
*/
const storyCarouselItemSchema = v.pipe(v.any(), v.transform((item) => ({
account_id: item.pid,
story_id: item.sid,
...item,
})), v.object({
account_id: v.string(),
avatar: v.string(),
local: v.boolean(),
username: v.string(),
latest: v.object({
id: v.pipe(v.unknown(), v.transform(String)),
type: v.string(),
preview_url: v.string(),
}),
url: v.string(),
seen: v.boolean(),
story_id: v.pipe(v.unknown(), v.transform(String)),
}));
/**
* @category Entity types
*/
type StoryCarouselItem = v.InferOutput<typeof storyCarouselItemSchema>;
export { storyCarouselItemSchema, type StoryCarouselItem };

View File

@ -0,0 +1,21 @@
import * as v from 'valibot';
/**
* @category Schemas
*/
const storyMediaSchema = v.pipe(v.any(), v.transform((media) => ({
id: media.media_id,
url: media.media_url,
type: media.media_type,
})), v.object({
id: v.string(),
url: v.string(),
type: v.picklist(['photo', 'video']),
}));
/**
* @category Entity types
*/
type StoryMedia = v.InferOutput<typeof storyMediaSchema>;
export { storyMediaSchema, type StoryMedia };

View File

@ -0,0 +1,45 @@
import * as v from 'valibot';
import { accountSchema } from './account';
import { datetimeSchema, filteredArray } from './utils';
/**
* @category Schemas
*/
const storyNodeSchema = v.object({
id: v.string(),
type: v.string(),
duration: v.number(),
src: v.string(),
created_at: datetimeSchema,
expires_at: datetimeSchema,
view_count: v.fallback(v.nullable(v.number()), null),
seen: v.boolean(),
progress: v.number(),
can_reply: v.boolean(),
can_react: v.boolean(),
question: v.optional(v.string(), undefined),
options: v.optional(v.array(v.string()), undefined),
voted: v.optional(v.boolean(), undefined),
voted_index: v.optional(v.number(), undefined),
});
/**
* @category Entity types
*/
type StoryNode = v.InferOutput<typeof storyNodeSchema>;
/**
* @category Schemas
*/
const storyProfileSchema = v.object({
account: accountSchema,
nodes: filteredArray(storyNodeSchema),
});
/**
* @category Entity types
*/
type StoryProfile = v.InferOutput<typeof storyProfileSchema>;
export { storyNodeSchema, type StoryNode, storyProfileSchema, type StoryProfile };

View File

@ -1556,6 +1556,25 @@ const getFeatures = (instance: Instance) => {
*/
statusDislikes: v.software === FRIENDICA && gte(v.version, '2023.3.0'),
/**
* @see GET /api/web/stories/v1/recent
* @see GET /api/web/stories/v1/viewers
* @see GET /api/web/stories/v1/profile/:id
* @see GET /api/web/stories/v1/exists/:id
* @see GET /api/web/stories/v1/poll/results
* @see POST /api/web/stories/v1/viewed
* @see POST /api/web/stories/v1/react
* @see POST /api/web/stories/v1/comment
* @see POST /api/web/stories/v1/publish/poll
* @see POST /api/web/stories/v1/poll/vote
* @see POST /api/web/stories/v1/report
* @see POST /api/web/stories/v1/add
* @see POST /api/web/stories/v1/crop
* @see POST /api/web/stories/v1/publish
* @see DELETE /api/web/stories/v1/delete/:id
*/
stories: v.software === PIXELFED,
/**
* @see GET /api/v1/accounts/:id/subscribers
* @see POST /api/v1/subscriptions

View File

@ -0,0 +1,26 @@
interface CreateStoryPollParams {
/** From 6 to 140 characters. */
question: string;
/** Between 2 and 4 answers. */
answers: string;
can_reply: boolean;
can_react: boolean;
}
type StoryReportType = 'spam' | 'sensitive' | 'abusive' | 'underage' | 'copyright' | 'impersonation' | 'scam' | 'terrorism';
interface CropStoryPhotoParams {
width: number;
height: number;
x: number;
y: number;
}
interface CreateStoryParams {
/** Between 3 and 120 (in seconds). */
duration: number;
can_reply: boolean;
can_react: boolean;
}
export type { CreateStoryPollParams, StoryReportType, CropStoryPhotoParams, CreateStoryParams };