Merge branch 'mastodon-api-emoji-reactions' into 'develop'
Support proposed Glitch emoji reactions API Closes #10537 See merge request firefish/firefish!10532
This commit is contained in:
commit
8ff1c9b722
@ -32,6 +32,8 @@ export function convertNotification(notification: Entity.Notification) {
|
||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||
if (notification.status)
|
||||
notification.status = convertStatus(notification.status);
|
||||
if (notification.reaction)
|
||||
notification.reaction = convertReaction(notification.reaction);
|
||||
return notification;
|
||||
}
|
||||
|
||||
@ -68,7 +70,7 @@ export function convertStatus(status: Entity.Status) {
|
||||
if (status.poll) status.poll = convertPoll(status.poll);
|
||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
||||
if (status.quote) status.quote = convertStatus(status.quote);
|
||||
status.emoji_reactions = status.mentions.map(convertReaction);
|
||||
status.reactions = status.reactions.map(convertReaction);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||
try {
|
||||
const id = body.in_reply_to_id;
|
||||
const post = await client.getStatus(id);
|
||||
const react = post.data.emoji_reactions.filter((e) => e.me)[0].name;
|
||||
const react = post.data.reactions.filter((e) => e.me)[0].name;
|
||||
const data = await client.deleteEmojiReaction(id, react);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
@ -367,6 +367,47 @@ export function apiStatusMastodon(router: Router): void {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string; name: string } }>(
|
||||
"/v1/statuses/:id/react/:name",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.reactStatus(
|
||||
convertId(ctx.params.id, IdType.FirefishId),
|
||||
ctx.params.name,
|
||||
);
|
||||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string; name: string } }>(
|
||||
"/v1/statuses/:id/unreact/:name",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unreactStatus(
|
||||
convertId(ctx.params.id, IdType.FirefishId),
|
||||
ctx.params.name,
|
||||
);
|
||||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
|
@ -7,7 +7,7 @@ namespace Entity {
|
||||
created_at: string;
|
||||
id: string;
|
||||
status?: Status;
|
||||
emoji?: string;
|
||||
reaction?: Reaction;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,7 @@ namespace Entity {
|
||||
me: boolean;
|
||||
name: string;
|
||||
url?: string;
|
||||
static_url?: string;
|
||||
accounts?: Array<Account>;
|
||||
};
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace Entity {
|
||||
application: Application | null;
|
||||
language: string | null;
|
||||
pinned: boolean | null;
|
||||
emoji_reactions: Array<Reaction>;
|
||||
reactions: Array<Reaction>;
|
||||
quote: Status | null;
|
||||
bookmarked: boolean;
|
||||
};
|
||||
|
@ -857,6 +857,21 @@ export interface MegalodonInterface {
|
||||
* @return Status
|
||||
*/
|
||||
unpinStatus(id: string): Promise<Response<Entity.Status>>;
|
||||
/**
|
||||
* POST /api/v1/statuses/:id/react/:name
|
||||
* @param id The target status id.
|
||||
* @param name The name of the emoji reaction to add.
|
||||
* @return Status
|
||||
*/
|
||||
reactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
|
||||
/**
|
||||
* POST /api/v1/statuses/:id/unreact/:name
|
||||
*
|
||||
* @param id The target status id.
|
||||
* @param name The name of the emoji reaction to remove.
|
||||
* @return Status
|
||||
*/
|
||||
unreactStatus(id: string, name: string): Promise<Response<Entity.Status>>;
|
||||
// ======================================
|
||||
// statuses/media
|
||||
// ======================================
|
||||
|
@ -2009,6 +2009,63 @@ export default class Misskey implements MegalodonInterface {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Unicode emoji or custom emoji name to a Misskey reaction.
|
||||
* @see Misskey's reaction-lib.ts
|
||||
*/
|
||||
private reactionName(name: string): string {
|
||||
// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
|
||||
const isUnicodeEmoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu.test(name);
|
||||
if (isUnicodeEmoji) {
|
||||
return name;
|
||||
}
|
||||
return `:${name}:`;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/notes/reactions/create
|
||||
*/
|
||||
public async reactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
|
||||
await this.client.post<{}>("/api/notes/reactions/create", {
|
||||
noteId: id,
|
||||
reaction: this.reactionName(name),
|
||||
});
|
||||
return this.client
|
||||
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
||||
noteId: id,
|
||||
})
|
||||
.then(async (res) => ({
|
||||
...res,
|
||||
data: await this.noteWithDetails(
|
||||
res.data,
|
||||
this.baseUrlToHost(this.baseUrl),
|
||||
this.getFreshAccountCache(),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/notes/reactions/delete
|
||||
*/
|
||||
public async unreactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
|
||||
await this.client.post<{}>("/api/notes/reactions/delete", {
|
||||
noteId: id,
|
||||
reaction: this.reactionName(name),
|
||||
});
|
||||
return this.client
|
||||
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
||||
noteId: id,
|
||||
})
|
||||
.then(async (res) => ({
|
||||
...res,
|
||||
data: await this.noteWithDetails(
|
||||
res.data,
|
||||
this.baseUrlToHost(this.baseUrl),
|
||||
this.getFreshAccountCache(),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// statuses/media
|
||||
// ======================================
|
||||
|
@ -321,7 +321,10 @@ namespace MisskeyAPI {
|
||||
content: n.text ? this.escapeMFM(n.text) : "",
|
||||
plain_content: n.text ? n.text : null,
|
||||
created_at: n.createdAt,
|
||||
emojis: n.emojis.map((e) => this.emoji(e)),
|
||||
// Remove reaction emojis with names containing @ from the emojis list.
|
||||
emojis: n.emojis
|
||||
.filter((e) => e.name.indexOf("@") === -1)
|
||||
.map((e) => this.emoji(e)),
|
||||
replies_count: n.repliesCount,
|
||||
reblogs_count: n.renoteCount,
|
||||
favourites_count: this.getTotalReactions(n.reactions),
|
||||
@ -339,28 +342,36 @@ namespace MisskeyAPI {
|
||||
application: null,
|
||||
language: null,
|
||||
pinned: null,
|
||||
emoji_reactions: this.mapReactions(n.reactions, n.myReaction),
|
||||
// Use emojis list to provide URLs for emoji reactions.
|
||||
reactions: this.mapReactions(n.emojis, n.reactions, n.myReaction),
|
||||
bookmarked: false,
|
||||
quote: n.renote && n.text ? this.note(n.renote, host) : null,
|
||||
};
|
||||
};
|
||||
|
||||
mapReactions = (
|
||||
emojis: Array<MisskeyEntity.Emoji>,
|
||||
r: { [key: string]: number },
|
||||
myReaction?: string,
|
||||
): Array<MegalodonEntity.Reaction> => {
|
||||
// Map of emoji shortcodes to image URLs.
|
||||
const emojiUrls = new Map<string, string>(
|
||||
emojis.map((e) => [e.name, e.url]),
|
||||
);
|
||||
return Object.keys(r).map((key) => {
|
||||
if (myReaction && key === myReaction) {
|
||||
return {
|
||||
count: r[key],
|
||||
me: true,
|
||||
name: key,
|
||||
};
|
||||
}
|
||||
// Strip colons from custom emoji reaction names to match emoji shortcodes.
|
||||
const shortcode = key.replaceAll(":", "");
|
||||
// If this is a custom emoji (vs. a Unicode emoji), find its image URL.
|
||||
const url = emojiUrls.get(shortcode);
|
||||
// Finally, remove trailing @. from local custom emoji reaction names.
|
||||
const name = shortcode.replace("@.", "");
|
||||
return {
|
||||
count: r[key],
|
||||
me: false,
|
||||
name: key,
|
||||
me: key === myReaction,
|
||||
name,
|
||||
url,
|
||||
// We don't actually have a static version of the asset, but clients expect one anyway.
|
||||
static_url: url,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -422,7 +433,7 @@ namespace MisskeyAPI {
|
||||
case NotificationType.Mention:
|
||||
return MisskeyNotificationType.Reply;
|
||||
case NotificationType.Favourite:
|
||||
case NotificationType.EmojiReaction:
|
||||
case NotificationType.Reaction:
|
||||
return MisskeyNotificationType.Reaction;
|
||||
case NotificationType.Reblog:
|
||||
return MisskeyNotificationType.Renote;
|
||||
@ -448,7 +459,7 @@ namespace MisskeyAPI {
|
||||
case MisskeyNotificationType.Quote:
|
||||
return NotificationType.Reblog;
|
||||
case MisskeyNotificationType.Reaction:
|
||||
return NotificationType.EmojiReaction;
|
||||
return NotificationType.Reaction;
|
||||
case MisskeyNotificationType.PollEnded:
|
||||
return NotificationType.Poll;
|
||||
case MisskeyNotificationType.ReceiveFollowRequest:
|
||||
@ -496,11 +507,11 @@ namespace MisskeyAPI {
|
||||
account: this.note(n.note, host).account,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (n.reaction) {
|
||||
notification = Object.assign(notification, {
|
||||
emoji: n.reaction,
|
||||
});
|
||||
if (n.reaction) {
|
||||
notification = Object.assign(notification, {
|
||||
reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
return notification;
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ namespace NotificationType {
|
||||
export const Favourite: Entity.NotificationType = "favourite";
|
||||
export const Reblog: Entity.NotificationType = "reblog";
|
||||
export const Mention: Entity.NotificationType = "mention";
|
||||
export const EmojiReaction: Entity.NotificationType = "emoji_reaction";
|
||||
export const Reaction: Entity.NotificationType = "reaction";
|
||||
export const FollowRequest: Entity.NotificationType = "follow_request";
|
||||
export const Status: Entity.NotificationType = "status";
|
||||
export const Poll: Entity.NotificationType = "poll";
|
||||
|
@ -163,7 +163,7 @@ describe('getNotifications', () => {
|
||||
},
|
||||
{
|
||||
event: reaction,
|
||||
expected: MegalodonNotificationType.EmojiReaction,
|
||||
expected: MegalodonNotificationType.Reaction,
|
||||
title: 'reaction'
|
||||
},
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ describe('api_client', () => {
|
||||
dist: MisskeyNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.EmojiReaction,
|
||||
src: MegalodonNotificationType.Reaction,
|
||||
dist: MisskeyNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
@ -80,7 +80,7 @@ describe('api_client', () => {
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Reaction,
|
||||
dist: MegalodonNotificationType.EmojiReaction
|
||||
dist: MegalodonNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.PollEnded,
|
||||
|
@ -54,7 +54,7 @@ const status: Entity.Status = {
|
||||
} as Entity.Application,
|
||||
language: null,
|
||||
pinned: null,
|
||||
emoji_reactions: [],
|
||||
reactions: [],
|
||||
bookmarked: false,
|
||||
quote: null
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
/* Basic Options */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["es6", "dom"], /* Specify library files to be included in the compilation. */
|
||||
"lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
|
Loading…
Reference in New Issue
Block a user