pl-api: Start implemenitng grouped notifications with fallback to v1

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-01 23:19:18 +01:00
parent 199c43d241
commit 94b3af03e9
10 changed files with 128 additions and 19 deletions

View file

@ -1,3 +1,4 @@
import omit from 'lodash.omit';
import * as v from 'valibot';
import {
@ -27,6 +28,7 @@ import {
contextSchema,
conversationSchema,
credentialAccountSchema,
credentialApplicationSchema,
customEmojiSchema,
domainBlockSchema,
emojiReactionSchema,
@ -70,6 +72,7 @@ import {
trendsLinkSchema,
webPushSubscriptionSchema,
} from './entities';
import { GroupedNotificationsResults, groupedNotificationsResultsSchema, NotificationGroup } from './entities/grouped-notifications-results';
import { filteredArray } from './entities/utils';
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, MITRA } from './features';
import {
@ -107,6 +110,7 @@ import {
MuteAccountParams,
UpdateFilterParams,
} from './params/filtering';
import { GetGroupedNotificationsParams } from './params/grouped-notifications';
import {
CreateGroupParams,
GetGroupBlocksParams,
@ -195,6 +199,7 @@ import type {
AdminReport,
GroupRole,
Instance,
Notification,
PleromaConfig,
Status,
StreamingEvent,
@ -228,7 +233,9 @@ import type {
AdminUpdateRuleParams,
AdminUpdateStatusParams,
} from './params/admin';
import type { PaginatedResponse } from './responses';
import type { PaginatedResponse, PaginatedSingleResponse } from './responses';
const GROUPED_TYPES = ['favourite', 'reblog', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'];
/**
* @category Clients
@ -353,6 +360,60 @@ class PlApiClient {
};
};
#groupNotifications = ({ previous, next, items, ...response }: PaginatedResponse<Notification>, params?: GetGroupedNotificationsParams): PaginatedSingleResponse<GroupedNotificationsResults> => {
const notificationGroups: Array<NotificationGroup> = [];
for (const notification of items) {
let existingGroup: NotificationGroup | undefined;
if ((params?.grouped_types || GROUPED_TYPES).includes(notification.type)) {
existingGroup = notificationGroups
.find(notificationGroup =>
notificationGroup.type === notification.type
&& ((notification.type === 'emoji_reaction' && notificationGroup.type === 'emoji_reaction') ? notification.emoji === notificationGroup.emoji : true)
// @ts-ignore
&& notificationGroup.status_id === notification.status?.id,
);
}
if (existingGroup) {
existingGroup.notifications_count += 1;
existingGroup.page_min_id = notification.id;
existingGroup.sample_account_ids.push(notification.account.id);
} else {
notificationGroups.push({
...(omit(notification, ['account', 'status', 'target'])),
group_key: notification.id,
notifications_count: 1,
most_recent_notification_id: notification.id,
page_min_id: notification.id,
page_max_id: notification.id,
latest_page_notification_at: notification.created_at,
sample_account_ids: [notification.account.id],
// @ts-ignore
status_id: notification.status?.id,
// @ts-ignore
target_id: notification.target?.id,
});
}
}
const groupedNotificationsResults: GroupedNotificationsResults = {
accounts: items.map(({ account }) => account),
statuses: items.reduce<Array<Status>>((statuses, notification) => {
if ('status' in notification) statuses.push(notification.status);
return statuses;
}, []),
notification_groups: notificationGroups,
};
return {
...response,
previous: previous ? async () => this.#groupNotifications(await previous(), params) : null,
next: next ? async () => this.#groupNotifications(await next(), params) : null,
items: groupedNotificationsResults,
};
};
/** Register client applications that can be used to obtain OAuth tokens. */
public readonly apps = {
/**
@ -363,7 +424,7 @@ class PlApiClient {
createApplication: async (params: CreateApplicationParams) => {
const response = await this.request('/api/v1/apps', { method: 'POST', body: params });
return v.parse(applicationSchema, response.json);
return v.parse(credentialApplicationSchema, response.json);
},
/**
@ -2705,6 +2766,16 @@ class PlApiClient {
};
public readonly groupedNotifications = {
getGroupedNotifications: async (params: GetGroupedNotificationsParams) => {
if (this.features.groupedNotifications) {
return this.#paginatedGet('/api/v2/notifications', { params }, groupedNotificationsResultsSchema);
} else {
return this.#groupNotifications(await this.notifications.getNotifications(), params);
}
},
};
public readonly pushNotifications = {
/**
* Subscribe to push notifications

View file

@ -12,9 +12,6 @@ const applicationSchema = v.pipe(v.any(), v.transform((application) => ({
})), v.object({
name: v.fallback(v.string(), ''),
website: v.fallback(v.optional(v.string()), undefined),
client_id: v.fallback(v.optional(v.string()), undefined),
client_secret: v.fallback(v.optional(v.string()), undefined),
client_secret_expires_at: v.fallback(v.optional(v.string()), undefined),
redirect_uris: filteredArray(v.string()),
id: v.fallback(v.optional(v.string()), undefined),
@ -27,4 +24,21 @@ const applicationSchema = v.pipe(v.any(), v.transform((application) => ({
type Application = v.InferOutput<typeof applicationSchema>;
export { applicationSchema, type Application };
/**
* @category Schemas
* @see {@link https://docs.joinmastodon.org/entities/Application/#CredentialApplication}
*/
const credentialApplicationSchema = v.pipe(
applicationSchema.pipe[0],
applicationSchema.pipe[1],
v.object({
...applicationSchema.pipe[2].entries,
client_id: v.string(),
client_secret: v.string(),
client_secret_expires_at: v.fallback(v.optional(v.string()), undefined),
}),
);
type CredentialApplication = v.InferOutput<typeof credentialApplicationSchema>;
export { applicationSchema, credentialApplicationSchema, type Application, type CredentialApplication };

View file

@ -619,6 +619,15 @@ const getFeatures = (instance: Instance) => {
v.software === PLEROMA,
]),
/**
* @see GET /api/v2/notifications/:group_key
* @see GET /api/v2/notifications/:group_key
* @see POST /api/v2/notifications/:group_key/dismiss
* @see GET /api/v2/notifications/:group_key/accounts
* @see GET /api/v2/notifications/unread_count
*/
groupedNotifications: instance.api_versions.mastodon >= 2,
/**
* Groups.
* @see POST /api/v1/groups

View file

@ -1,11 +1,14 @@
interface PaginatedResponse<T> {
previous: (() => Promise<PaginatedResponse<T>>) | null;
next: (() => Promise<PaginatedResponse<T>>) | null;
items: Array<T>;
interface PaginatedSingleResponse<T> {
previous: (() => Promise<PaginatedSingleResponse<T>>) | null;
next: (() => Promise<PaginatedSingleResponse<T>>) | null;
items: T;
partial: boolean;
total?: number;
}
type PaginatedResponse<T> = PaginatedSingleResponse<Array<T>>;
export type {
PaginatedSingleResponse,
PaginatedResponse,
};

View file

@ -20,6 +20,7 @@
"devDependencies": {
"@stylistic/eslint-plugin": "^2.8.0",
"@types/http-link-header": "^1.0.7",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pick": "^4.4.9",
"@types/node": "^22.7.4",
"@types/semver": "^7.5.8",
@ -41,6 +42,7 @@
"dependencies": {
"blurhash": "^2.0.5",
"http-link-header": "^1.1.3",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"object-to-formdata": "^4.5.1",
"query-string": "^9.1.0",

View file

@ -478,6 +478,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash.omit@^4.5.9":
version "4.5.9"
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.9.tgz#cf4744d034961406d6dc41d9cd109773a9ed8fe3"
integrity sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw==
dependencies:
"@types/lodash" "*"
"@types/lodash.pick@^4.4.9":
version "4.4.9"
resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.9.tgz#06f7d88faa81a6c5665584778aea7b1374a1dc5b"
@ -2002,6 +2009,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.omit@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"

View file

@ -47,11 +47,11 @@ const externalAuthorize = (instance: Instance, baseURL: string) =>
const scopes = getInstanceScopes(instance);
return dispatch(createExternalApp(instance, baseURL)).then((app) => {
const { client_id, redirect_uri } = app as Record<string, string>;
const { client_id, redirect_uri } = app;
const query = new URLSearchParams({
client_id,
redirect_uri,
redirect_uri: redirect_uri || app.redirect_uris[0]!,
response_type: 'code',
scope: scopes,
});

View file

@ -13,8 +13,6 @@ import type { AppDispatch, RootState } from 'pl-fe/store';
const FE_NAME = 'pl_fe';
const getAccount = makeGetAccount();
/** Options when changing/saving settings. */
type SettingOpts = {
/** Whether to display an alert when settings are saved. */
@ -71,7 +69,7 @@ const updateSettingsStore = (settings: any) =>
},
}));
} else {
const accountUrl = getAccount(state, state.me as string)!.url;
const accountUrl = makeGetAccount()(state, state.me as string)!.url;
return updateAuthAccount(accountUrl, settings);
}

View file

@ -16,7 +16,7 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
import { getBaseURL } from 'pl-fe/utils/accounts';
import type { Token } from 'pl-api';
import type { CredentialApplication, Token } from 'pl-api';
const messages = defineMessages({
heading: { id: 'column.app_create', defaultMessage: 'Create app' },
@ -53,7 +53,7 @@ const CreateApp: React.FC = () => {
});
};
const handleCreateToken = (app: Record<string, string>) => {
const handleCreateToken = (app: CredentialApplication) => {
const baseURL = getBaseURL(account!);
const tokenParams = {

View file

@ -1,6 +1,6 @@
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
import trim from 'lodash/trim';
import { applicationSchema, PlApiClient, tokenSchema, type Application, type CredentialAccount, type Token } from 'pl-api';
import { applicationSchema, PlApiClient, tokenSchema, type CredentialAccount, type CredentialApplication, type Token } from 'pl-api';
import * as v from 'valibot';
import { MASTODON_PRELOAD_IMPORT, type PreloadAction } from 'pl-fe/actions/preload';
@ -33,7 +33,7 @@ const AuthUserRecord = ImmutableRecord({
});
const ReducerRecord = ImmutableRecord({
app: null as Application | null,
app: null as CredentialApplication | null,
tokens: ImmutableMap<string, Token>(),
users: ImmutableMap<string, AuthUser>(),
me: null as string | null,