support reactions in mastodon API

This commit is contained in:
Hazelnoot 2025-03-22 18:18:54 -04:00
parent fbdee815da
commit 3c54680860
8 changed files with 82 additions and 54 deletions

View file

@ -180,10 +180,10 @@ export class MastoConverters {
note: profile?.description ?? '',
url: user.uri ?? acctUrl,
uri: user.uri ?? acctUri,
avatar: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
avatar_static: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
header: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
header_static: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
avatar: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png',
avatar_static: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png',
header: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png',
header_static: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png',
emojis: emoji,
moved: null, //FIXME
fields: profile?.fields.map(p => this.encodeField(p)) ?? [],
@ -196,7 +196,7 @@ export class MastoConverters {
});
}
public async getEdits(id: string, me?: MiLocalUser | null): Promise<StatusEdit[]> {
public async getEdits(id: string, me: MiLocalUser | null): Promise<StatusEdit[]> {
const note = await this.mastodonDataService.getNote(id, me);
if (!note) {
return [];
@ -213,7 +213,7 @@ export class MastoConverters {
account: noteUser,
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '',
created_at: lastDate.toISOString(),
emojis: [],
emojis: [], //FIXME
sensitive: edit.cw != null && edit.cw.length > 0,
spoiler_text: edit.cw ?? '',
media_attachments: files.length > 0 ? files.map((f) => this.encodeFile(f)) : [],
@ -222,15 +222,15 @@ export class MastoConverters {
history.push(item);
}
return await Promise.all(history);
return history;
}
private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
private async convertReblog(status: Entity.Status | null, me: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
if (!status) return null;
return await this.convertStatus(status, me);
}
public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise<MastodonEntity.Status> {
public async convertStatus(status: Entity.Status, me: MiLocalUser | null): Promise<MastodonEntity.Status> {
const convertedAccount = this.convertAccount(status.account);
const note = await this.mastodonDataService.requireNote(status.id, me);
const noteUser = await this.getUser(status.account.id);
@ -279,7 +279,6 @@ export class MastoConverters {
: '';
const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
const reactions = await Promise.all(status.emoji_reactions.map(r => this.convertReaction(r)));
// noinspection ES6MissingAwait
return await awaitAll({
@ -289,11 +288,12 @@ export class MastoConverters {
account: convertedAccount,
in_reply_to_id: note.replyId,
in_reply_to_account_id: note.replyUserId,
reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null,
reblog: !isQuote ? this.convertReblog(status.reblog, me) : null,
content: content,
content_type: 'text/x.misskeymarkdown',
text: note.text,
created_at: status.created_at,
edited_at: note.updatedAt?.toISOString() ?? null,
emojis: emoji,
replies_count: note.repliesCount,
reblogs_count: note.renoteCount,
@ -301,7 +301,7 @@ export class MastoConverters {
reblogged,
favourited: status.favourited,
muted: status.muted,
sensitive: status.sensitive,
sensitive: status.sensitive || !!note.cw,
spoiler_text: note.cw ?? '',
visibility: status.visibility,
media_attachments: status.media_attachments.map(a => convertAttachment(a)),
@ -312,15 +312,14 @@ export class MastoConverters {
application: null, //FIXME
language: null, //FIXME
pinned: false, //FIXME
reactions,
emoji_reactions: reactions,
bookmarked: false, //FIXME
quote: isQuote ? await this.convertReblog(status.reblog, me) : null,
edited_at: note.updatedAt?.toISOString() ?? null,
quote_id: isQuote ? status.reblog?.id : undefined,
quote: isQuote ? this.convertReblog(status.reblog, me) : null,
reactions: status.emoji_reactions,
});
}
public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
public async convertConversation(conversation: Entity.Conversation, me: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
return {
id: conversation.id,
accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))),
@ -329,7 +328,7 @@ export class MastoConverters {
};
}
public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise<MastodonEntity.Notification> {
public async convertNotification(notification: Entity.Notification, me: MiLocalUser | null): Promise<MastodonEntity.Notification> {
return {
account: await this.convertAccount(notification.account),
created_at: notification.created_at,
@ -339,12 +338,23 @@ export class MastoConverters {
};
}
public async convertReaction(reaction: Entity.Reaction): Promise<Entity.Reaction> {
if (reaction.accounts) {
reaction.accounts = await Promise.all(reaction.accounts.map(a => this.convertAccount(a)));
}
return reaction;
}
// public convertEmoji(emoji: string): MastodonEntity.Emoji {
// const reaction: MastodonEntity.Reaction = {
// name: emoji,
// count: 1,
// };
//
// if (emoji.startsWith(':')) {
// const [, name] = emoji.match(/^:([^@:]+(?:@[^@:]+)?):$/) ?? [];
// if (name) {
// const url = `${this.config.url}/emoji/${name}.webp`;
// reaction.url = url;
// reaction.static_url = url;
// }
// }
//
// return reaction;
// }
}
function simpleConvert<T>(data: T): T {
@ -423,7 +433,3 @@ export function convertRelationship(relationship: Partial<Entity.Relationship> &
};
}
// noinspection JSUnusedGlobalSymbols
export function convertStatusSource(status: Entity.StatusSource): MastodonEntity.StatusSource {
return simpleConvert(status);
}

View file

@ -29,13 +29,17 @@ export class ApiNotificationsMastodon {
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (request, reply) => {
const { client, me } = await this.clientService.getAuthClient(request);
const data = await client.getNotifications(parseTimelineArgs(request.query));
const response = await Promise.all(data.data.map(async n => {
const converted = await this.mastoConverters.convertNotification(n, me);
if (converted.type === 'reaction') {
converted.type = 'favourite';
const notifications = await Promise.all(data.data.map(n => this.mastoConverters.convertNotification(n, me)));
const response: MastodonEntity.Notification[] = [];
for (const notification of notifications) {
response.push(notification);
if (notification.type === 'reaction') {
response.push({
...notification,
type: 'favourite',
});
}
return converted;
}));
}
attachMinMaxPagination(request, reply, response);
reply.send(response);
@ -46,12 +50,9 @@ export class ApiNotificationsMastodon {
const { client, me } = await this.clientService.getAuthClient(_request);
const data = await client.getNotification(_request.params.id);
const converted = await this.mastoConverters.convertNotification(data.data, me);
if (converted.type === 'reaction') {
converted.type = 'favourite';
}
const response = await this.mastoConverters.convertNotification(data.data, me);
reply.send(converted);
reply.send(response);
});
fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {

View file

@ -6,5 +6,7 @@ namespace Entity {
me: boolean
name: string
accounts?: Array<Account>
url?: string
static_url?: string
}
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
/// <reference path="account.ts" />
namespace MastodonEntity {
export type Reaction = {
name: string
count: number
me?: boolean
url?: string
static_url?: string
}
}

View file

@ -6,6 +6,7 @@
/// <reference path="emoji.ts" />
/// <reference path="card.ts" />
/// <reference path="poll.ts" />
/// <reference path="reaction.ts" />
namespace MastodonEntity {
export type Status = {
@ -41,6 +42,8 @@ namespace MastodonEntity {
// These parameters are unique parameters in fedibird.com for quote.
quote_id?: string
quote?: Status | null
// These parameters are unique to glitch-soc for emoji reactions.
reactions?: Reaction[]
}
export type StatusTag = {

View file

@ -22,6 +22,7 @@
/// <reference path="./entities/poll_option.ts" />
/// <reference path="./entities/preferences.ts" />
/// <reference path="./entities/push_subscription.ts" />
/// <reference path="./entities/reaction.ts" />
/// <reference path="./entities/relationship.ts" />
/// <reference path="./entities/report.ts" />
/// <reference path="./entities/results.ts" />

View file

@ -2555,6 +2555,7 @@ export default class Misskey implements MegalodonInterface {
}))
}
// TODO implement
public async getEmojiReaction(_id: string, _emoji: string): Promise<Response<Entity.Reaction>> {
return new Promise((_, reject) => {
const err = new NoImplementedError('misskey does not support')

View file

@ -286,6 +286,7 @@ namespace MisskeyAPI {
plain_content: n.text ? n.text : null,
created_at: n.createdAt,
edited_at: n.updatedAt || null,
// TODO this is probably wrong
emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)),
replies_count: n.repliesCount,
reblogs_count: n.renoteCount,
@ -304,7 +305,7 @@ namespace MisskeyAPI {
application: null,
language: null,
pinned: null,
emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [],
emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.reactionEmojis, n.myReaction) : [],
bookmarked: false,
quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : null
}
@ -334,23 +335,20 @@ namespace MisskeyAPI {
) : 0;
};
export const mapReactions = (r: { [key: string]: number }, myReaction?: string): Array<MegalodonEntity.Reaction> => {
export const mapReactions = (r: { [key: string]: number }, e: Record<string, string | undefined>, myReaction?: string): Array<MegalodonEntity.Reaction> => {
return Object.keys(r).map(key => {
if (myReaction && key === myReaction) {
return {
count: r[key],
me: true,
name: key
}
}
return {
count: r[key],
me: false,
name: key
}
const me = myReaction != null && key === myReaction;
return {
count: r[key],
me,
name: key,
url: e[key],
static_url: e[key],
}
})
}
// TODO implement other properties
const mapReactionEmojis = (r: { [key: string]: string }): Array<MegalodonEntity.Emoji> => {
return Object.keys(r).map(key => ({
shortcode: key,
@ -371,7 +369,7 @@ namespace MisskeyAPI {
result.push({
count: 1,
me: false,
name: e.type
name: e.type,
})
}
})