diff --git a/application/prisma/migrations/20220108001028_plan_misskey_refresh/migration.sql b/application/prisma/migrations/20220108001028_plan_misskey_refresh/migration.sql new file mode 100644 index 0000000..37980dc --- /dev/null +++ b/application/prisma/migrations/20220108001028_plan_misskey_refresh/migration.sql @@ -0,0 +1,4 @@ +update "Node" +set "refreshedAt"=NULL, + "refreshAttemptedAt"=NULL +where "Node"."softwareName" like 'misskey'; diff --git a/application/src/Fediverse/Providers/Misskey/index.ts b/application/src/Fediverse/Providers/Misskey/index.ts new file mode 100644 index 0000000..8241eeb --- /dev/null +++ b/application/src/Fediverse/Providers/Misskey/index.ts @@ -0,0 +1,19 @@ +import { Provider } from '../Provider' +import { NodeProvider } from '../NodeProvider' +import { FeedProvider } from '../FeedProvider' +import { retrieveInstancesPage } from './retrieveInstancesPage' +import { retrieveUsersPage } from './retrieveUsersPage' + +const MisskeyProvider: Provider = { + getKey: () => 'misskey', + getNodeProviders: ():NodeProvider[] => [{ + getKey: () => 'federation-instances', + retrieveNodes: retrieveInstancesPage + }], + getFeedProviders: ():FeedProvider[] => [{ + getKey: () => 'users', + retrieveFeeds: retrieveUsersPage + }] +} + +export default MisskeyProvider diff --git a/application/src/Fediverse/Providers/Misskey/retrieveInstancesPage.ts b/application/src/Fediverse/Providers/Misskey/retrieveInstancesPage.ts new file mode 100644 index 0000000..0b652c7 --- /dev/null +++ b/application/src/Fediverse/Providers/Misskey/retrieveInstancesPage.ts @@ -0,0 +1,43 @@ +import axios from 'axios' +import { assertSuccessJsonResponse } from '../../assertSuccessJsonResponse' +import { z } from 'zod' +import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds' + +const limit = 100 + +const schema = z.array( + z.object({ + host: z.string() + }) +) + +export const retrieveInstancesPage = async (domain: string, page: number): Promise => { + try { + const response = await axios.post('https://' + domain + '/api/federation/instances', { + host: null, + blocked: null, + notResponding: null, + suspended: null, + federating: null, + subscribing: null, + publishing: null, + limit: limit, + offset: page * limit, + sort: '+id' + }, { + timeout: getDefaultTimeoutMilliseconds() + }) + assertSuccessJsonResponse(response) + const responseData = schema.parse(response.data) + if (responseData.length === 0) { + throw new Error('No more instances') + } + return responseData.map( + item => { + return item.host + } + ) + } catch (error) { + throw new Error('Invalid response: ' + error) + } +} diff --git a/application/src/Fediverse/Providers/Misskey/retrieveUsersPage.ts b/application/src/Fediverse/Providers/Misskey/retrieveUsersPage.ts new file mode 100644 index 0000000..8ddc4ab --- /dev/null +++ b/application/src/Fediverse/Providers/Misskey/retrieveUsersPage.ts @@ -0,0 +1,116 @@ +import axios from 'axios' +import { assertSuccessJsonResponse } from '../../assertSuccessJsonResponse' +import { FeedData } from '../FeedData' +import { z } from 'zod' +import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds' + +const limit = 100 + +const emojiSchema = z.object({ + name: z.string(), + url: z.string() +}) + +const schema = z.array( + z.object({ + id: z.string(), + name: z.string().nullable(), + username: z.string(), + avatarUrl: z.string(), + isBot: z.boolean(), + emojis: z.array(emojiSchema), + createdAt: z.string(), + updatedAt: z.string(), + isLocked: z.boolean(), + description: z.string().nullable(), + location: z.string().nullable(), + birthday: z.string().nullable(), + lang: z.string().nullable(), + fields: z.array( + z.object({ + name: z.string(), + value: z.string() + }) + ), + followersCount: z.number(), + followingCount: z.number(), + notesCount: z.number() + }) +) + +type Emoji = z.infer + +const replaceEmojis = (text: string, emojis: Emoji[]): string => { + emojis.forEach(emoji => { + text = text.replace( + RegExp(`:${emoji.name}:`, 'gi'), + `${emoji.name}` + ) + }) + return text +} + +const parseDescription = (description:string|null):string => { + if (typeof description !== 'string') { + return '' + } + return description.split('\n\n').map(paragraph => { + paragraph = paragraph.replace('\n', '
\n') + return `

${paragraph}

` + }).join('\n') +} + +export const retrieveUsersPage = async (domain: string, page: number): Promise => { + try { + const response = await axios.post('https://' + domain + '/api/users', { + state: 'all', + origin: 'local', + sort: '+createdAt', + limit: limit, + offset: limit * page + }, { + timeout: getDefaultTimeoutMilliseconds() + }) + assertSuccessJsonResponse(response) + const responseData = schema.parse(response.data) + if (responseData.length === 0) { + throw new Error('No more users') + } + return responseData.map( + item => { + return { + name: item.username, + displayName: replaceEmojis(item.name ?? item.username, item.emojis), + description: replaceEmojis(parseDescription(item.description ?? ''), item.emojis), + followersCount: item.followersCount, + followingCount: item.followingCount, + statusesCount: item.notesCount, + bot: item.isBot, + url: `https://${domain}/@${item.username}`, + avatar: item.avatarUrl, + locked: item.isLocked, + lastStatusAt: item.updatedAt !== null ? new Date(item.updatedAt) : null, + createdAt: new Date(item.createdAt), + fields: [ + ...item.fields.map(field => { + return { + name: replaceEmojis(field.name, item.emojis), + value: replaceEmojis(field.value, item.emojis), + verifiedAt: null + } + }), + ...[ + { name: 'Location', value: item.location, verifiedAt: null }, + { name: 'Birthday', value: item.birthday, verifiedAt: null }, + { name: 'Language', value: item.lang, verifiedAt: null } + ].filter(field => field.value !== null) + ], + type: 'account', + parentFeed: null + } + } + ) + } catch (error) { + throw new Error('Invalid response: ' + error) + } +} diff --git a/application/src/Fediverse/Providers/index.ts b/application/src/Fediverse/Providers/index.ts index ea337c6..666ade2 100644 --- a/application/src/Fediverse/Providers/index.ts +++ b/application/src/Fediverse/Providers/index.ts @@ -2,9 +2,11 @@ import { providerRegistry } from './ProviderRegistry' import MastodonProvider from './Mastodon' import PeertubeProvider from './Peertube' import PleromaProvider from './Pleroma' +import MisskeyProvider from './Misskey' providerRegistry.registerProvider(MastodonProvider) providerRegistry.registerProvider(PeertubeProvider) providerRegistry.registerProvider(PleromaProvider) +providerRegistry.registerProvider(MisskeyProvider) export default providerRegistry