Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Glitch-soc/Firefish emoji reactions API #1921

Merged
merged 11 commits into from
Sep 24, 2023
1 change: 1 addition & 0 deletions megalodon/src/entities/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Entity {
id: string
status?: Status
emoji?: string
reaction?: Reaction
ThatOneCalculator marked this conversation as resolved.
Show resolved Hide resolved
type: NotificationType
target?: Account
}
Expand Down
2 changes: 2 additions & 0 deletions megalodon/src/entities/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Entity {
count: number
me: boolean
name: string
url?: string
static_url?: string
accounts?: Array<Account>
}
}
20 changes: 17 additions & 3 deletions megalodon/src/firefish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,19 @@ export default class Firefish implements MegalodonInterface {
.then(res => ({ ...res, data: FirefishAPI.Converter.note(res.data) }))
}

/**
* Convert a Unicode emoji or custom emoji name to a Firefish reaction.
* @see Firefish'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}:`;
}

// ======================================
// statuses/media
// ======================================
Expand Down Expand Up @@ -2179,7 +2192,7 @@ export default class Firefish implements MegalodonInterface {
public async createEmojiReaction(id: string, emoji: string): Promise<Response<Entity.Status>> {
await this.client.post<Record<never, never>>('/api/notes/reactions/create', {
noteId: id,
reaction: emoji
reaction: this.reactionName(emoji)
})
return this.client
.post<FirefishAPI.Entity.Note>('/api/notes/show', {
Expand All @@ -2191,9 +2204,10 @@ export default class Firefish implements MegalodonInterface {
/**
* POST /api/notes/reactions/delete
*/
public async deleteEmojiReaction(id: string, _emoji: string): Promise<Response<Entity.Status>> {
public async deleteEmojiReaction(id: string, emoji: string): Promise<Response<Entity.Status>> {
await this.client.post<Record<never, never>>('/api/notes/reactions/delete', {
noteId: id
noteId: id,
reaction: this.reactionName(emoji)
})
return this.client
.post<FirefishAPI.Entity.Note>('/api/notes/show', {
Expand Down
96 changes: 54 additions & 42 deletions megalodon/src/firefish/api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@ namespace FirefishAPI {
: '',
plain_content: n.text ? n.text : null,
created_at: n.createdAt,
emojis: Array.isArray(n.emojis) ? n.emojis.map(e => emoji(e)) : [],
// Remove reaction emojis with names containing @ from the emojis list.
emojis: Array.isArray(n.emojis) ? n.emojis
.filter((e) => e.name.indexOf("@") === -1)
.map((e) => emoji(e)) : [],
replies_count: n.repliesCount,
reblogs_count: n.renoteCount,
favourites_count: 0,
Expand All @@ -284,25 +287,34 @@ namespace FirefishAPI {
application: null,
language: null,
pinned: null,
emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [],
// Use emojis list to provide URLs for emoji reactions.
emoji_reactions: mapReactions(n.emojis!, n.reactions, n.myReaction),
bookmarked: false,
quote: n.renote !== undefined && n.text !== null
}
}

export const mapReactions = (r: { [key: string]: number }, myReaction?: string | null): Array<MegalodonEntity.Reaction> => {
export const mapReactions = (emojis: Array<FirefishEntity.Emoji>, r: { [key: string]: number }, myReaction?: string | null): 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.replace(/:/g, "");
// 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,

}
})
}
Expand Down Expand Up @@ -352,7 +364,7 @@ namespace FirefishAPI {
case NotificationType.Mention:
return FirefishNotificationType.Reply
case NotificationType.Favourite:
case NotificationType.EmojiReaction:
case NotificationType.Reaction:
return FirefishNotificationType.Reaction
case NotificationType.Reblog:
return FirefishNotificationType.Renote
Expand All @@ -378,7 +390,7 @@ namespace FirefishAPI {
case FirefishNotificationType.Quote:
return NotificationType.Reblog
case FirefishNotificationType.Reaction:
return NotificationType.EmojiReaction
return NotificationType.Reaction
case FirefishNotificationType.PollVote:
return NotificationType.PollVote
case FirefishNotificationType.ReceiveFollowRequest:
Expand Down Expand Up @@ -408,8 +420,8 @@ namespace FirefishAPI {
}
if (n.reaction) {
notification = Object.assign(notification, {
emoji: n.reaction
})
reaction: mapReactions(n.note!.emojis!, { [n.reaction]: 1 })[0],
});
}
return notification
}
Expand Down Expand Up @@ -555,8 +567,8 @@ namespace FirefishAPI {
]

/**
* Interface
*/
* Interface
*/
export interface Interface {
get<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
post<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
Expand All @@ -565,10 +577,10 @@ namespace FirefishAPI {
}

/**
* Firefish API client.
*
* Usign axios for request, you will handle promises.
*/
* Firefish API client.
*
* Usign axios for request, you will handle promises.
*/
export class Client implements Interface {
private accessToken: string | null
private baseUrl: string
Expand All @@ -577,11 +589,11 @@ namespace FirefishAPI {
private proxyConfig: ProxyConfig | false = false

/**
* @param baseUrl hostname or base URL
* @param accessToken access token from OAuth2 authorization
* @param userAgent UserAgent is specified in header on request.
* @param proxyConfig Proxy setting, or set false if don't use proxy.
*/
* @param baseUrl hostname or base URL
* @param accessToken access token from OAuth2 authorization
* @param userAgent UserAgent is specified in header on request.
* @param proxyConfig Proxy setting, or set false if don't use proxy.
*/
constructor(baseUrl: string, accessToken: string | null, userAgent: string = DEFAULT_UA, proxyConfig: ProxyConfig | false = false) {
this.accessToken = accessToken
this.baseUrl = baseUrl
Expand All @@ -592,8 +604,8 @@ namespace FirefishAPI {
}

/**
* GET request to firefish API.
**/
* GET request to firefish API.
**/
public async get<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
params: params,
Expand All @@ -619,11 +631,11 @@ namespace FirefishAPI {
}

/**
* POST request to firefish REST API.
* @param path relative path from baseUrl
* @param params Form data
* @param headers Request header object
*/
* POST request to firefish REST API.
* @param path relative path from baseUrl
* @param params Form data
* @param headers Request header object
*/
public async post<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
Expand Down Expand Up @@ -658,19 +670,19 @@ namespace FirefishAPI {
}

/**
* Cancel all requests in this instance.
* @returns void
*/
* Cancel all requests in this instance.
* @returns void
*/
public cancel(): void {
return this.abortController.abort()
}

/**
* Get connection and receive websocket connection for Firefish API.
*
* @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
* @param listId This parameter is required only list channel.
*/
* Get connection and receive websocket connection for Firefish API.
*
* @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
* @param listId This parameter is required only list channel.
*/
public socket(
channel: 'user' | 'localTimeline' | 'hybridTimeline' | 'globalTimeline' | 'conversation' | 'list',
listId?: string
Expand Down
1 change: 1 addition & 0 deletions megalodon/src/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace NotificationType {
export const Favourite: Entity.NotificationType = 'favourite'
export const Reblog: Entity.NotificationType = 'reblog'
export const Mention: Entity.NotificationType = 'mention'
export const Reaction: Entity.NotificationType = 'reaction'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add a new notification type? Is it used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add Reaction even though EmojiReaction exists? What's the difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly to stay in line with Firefish's reaction notification name. I don't think it's very necessary but helps with differentiating string-only reaction notifications (Pleroma) from Reaction object notifications

Copy link
Owner

@h3poteto h3poteto Sep 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I got it.
You mean
In Pleroma(current):
When we receive an EmojiReaction notification, it contains emoji field in the object (it does not contain reaction). And it should be NotificationType.EmojiReaction.
In Firefish:
When we receive a Reaction notification, it contains reaction field in the object (it does not contain emoji). And it should be NotificationType.Reaction.

Furthermore, in the future Pleroma, when we receive an EmojiReaction, it contains reaction field in the object instead of emoji field. And at that time, we can remove NotificationType.EmojiReaction and emoji field in the notification object.

Correct?

Copy link
Contributor Author

@ThatOneCalculator ThatOneCalculator Sep 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, exactly.

export const EmojiReaction: Entity.NotificationType = 'emoji_reaction'
export const FollowRequest: Entity.NotificationType = 'follow_request'
export const Status: Entity.NotificationType = 'status'
Expand Down