Skip to content

arakur/MisskeyNet

Repository files navigation

High-Level Wrapper in .NET for Misskey API

Misskey API の高レベルな .NET ラッパーです.

既存のライブラリとしては公式の実装 Misq があります. 本ライブラリでは上記ライブラリの機能に加え,MiAuth 認証方式および WebSocket によるストリーミング API の利用,さらに API レスポンス JSON の静的型を提供します. これらの機能により,Misskey クライアントや bot の開発を高レイヤのコーディングで平易に行うことができるようにすることを目指しています.

Remark

Misskey.Net.ApiTypes では,misskey-json.api.md および Misskey-hub に記載されている API の型情報に基づき,API レスポンスの型を定義しています. 実際のところ,これらのコードはドキュメントおよび misskey-js の実装を基に手動で作成されたものであり,テストが不十分であることに加え,misskey 本体のバージョンアップによって不正な型付けとなる可能性があります.

レスポンスに対する静的型付けが不正である場合,ApiTypes 下のクラスはメンバを取得しようとした段階で例外を throw します(従って,データを作成した時点では例外は発出されません). また,ApiTypes 下のクラスは内部で保持される JSON データに直接アクセスする機能を提供します:

let node: JsonNode = data.["property"]

もしライブラリの実装ミスまたはバージョンアップによる破壊的変更により不正な型付けがなされていることが確認された場合,一旦上記の代替手段を用いて JSON データを直接取得して用いてください. そのうえで,本リポジトリの Issue にて報告をお願いします.

現在,misskey-js の実装から API の型情報を抽出しコード生成を行うスクリプトの作成を検討しています.

Installation

NuGet から利用できます.

Usage

src/Example/FSharp/Program.fs および src/Example/CSharp/Program.cs に,ローカルタイムラインと通知を購読するサンプルコードがあります.

F#

open Misskey.Net.Uri
open Misskey.Net.HttpApi
open Misskey.Net.StreamingApi
open Misskey.Net.ApiTypes
open Misskey.Net.Permission

open System.Net.Http
open Microsoft.Extensions.DependencyInjection
open System.Net.WebSockets
open System.Threading.Tasks

//

// Name of this app.
let appName = "Example App"

// Hostname of Misskey instance.
let Host = "misskey.systems"

//

// Utility.
let await (task: Task<'T>) =
    task |> Async.AwaitTask |> Async.RunSynchronously

//

// Main program:

// Create IHttpClientFactory instance.
let client =
    ServiceCollection() // Create DI container.
        .AddHttpClient() // Add HttpClient to DI container.
        .BuildServiceProvider() // Build DI container.
        .GetService<IHttpClientFactory>() // Get IHttpClientFactory from DI container.

// Create HttpApi instance.
let httpApi = HttpApi(scheme = Https, host = Host, client = client)

// Get stats of Misskey instance.
let stats = httpApi.RequestApiAsync [ "stats" ] |> await |> Stats

printfn "--stats --"

printfn "stats: %i" <| stats.NotesCount

printfn "users: %i" <| stats.UsersCount

printfn "instances: %i" <| stats.Instances

let auth =
    task {
        // Authorize this app to Misskey instance with permission `read:notification`.
        do!
            httpApi.AuthorizeAsync(
                name = appName,
                permissions = [| Permission.Read <| PermissionKind.Notifications() |]
            )

        // Wait for authorization.
        return! httpApi.WaitCheckAsync()
    }
    |> await

if auth = false then
    failwith "authorization failed"

printfn "authorization succeeded"

task {
    // Create StreamingApi instance.
    use streamingApi =
        new StreamingApi(httpApi = httpApi, webSocket = new ClientWebSocket())

    // Connect to Misskey instance.
    do! streamingApi.ConnectStreamingAsync()

    printfn "connected"

    // Connect to local timeline.
    let! _channelConnection = streamingApi.ConnectChannelAsync(Channel.LocalTimeline())

    printfn "connected to the local timeline channel"

    // Connect to main channel.
    let! _channelConnection = streamingApi.ConnectChannelAsync(Channel.Main())

    printfn "connected to the main channel"

    while true do
        printfn "subscribing..."
        // Subscribe to global timeline.
        let! result = streamingApi.ReceiveAsync()

        match result with
        // A note or a renote is posted.
        | StreamMessage.Channel message ->
            let body = message.Body

            match body with
            | ChannelMessageBody.Note note ->
                let user = note.User

                let name = defaultArg user.Name "<no name>"

                match note.Renote with
                | None ->
                    let text = defaultArg note.Text "<image only>"
                    printfn "-- note --"
                    printfn "user: %s" name
                    printfn "text: %s" text
                | Some renote ->
                    let original = defaultArg renote.User.Name "<no name>"
                    let text = defaultArg renote.Text "<image only>"
                    printfn "-- renote --"
                    printfn "user: %s" original
                    printfn "renoted by: %s" name
                    printfn "text: %s" text
            | ChannelMessageBody.Notification notification ->
                match notification.Body with
                | Notification.Body.OfReaction reaction ->
                    let user = defaultArg reaction.User.Name "<no name>"
                    let text = defaultArg reaction.Note.Text "<image only>"
                    printfn "-- reaction --"
                    printfn "user: %s" user
                    printfn "reaction: %s" reaction.Reaction
                    printfn "note: %s" text
                | _ ->
                    printfn "-- other notification --"
                    printfn "%s" <| notification.Body.ToString()
            | _ -> printfn "other message %s" <| body.ToString()

        // Connected.
        | StreamMessage.Connected message ->
            printfn "-- connected --"
            printfn "id: %s" message.Id

        // Note updated.
        | StreamMessage.NoteUpdated message ->
            printfn "-- note updated --"
            printfn "id: %s" message.Id

            let body =
                message.BodyData.ToString()
                |> (fun s -> s.Substring(0, 30))
                |> (fun s -> s + "...")

            printfn "body: %s" body

        // Other messages.
        | StreamMessage.Other message -> printfn "-- other message --"

        return ()
}
|> await

C#

using Misskey.Net;
using Misskey.Net.HttpApi;
using Misskey.Net.StreamingApi;
using Misskey.Net.Permission;
using static Misskey.Net.ApiTypes;

using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Net.WebSockets;
using Microsoft.FSharp.Core;

//

// Name of this app.
var appName = "Example app";

// Hostname of Misskey instance.
var host = "misskey.systems";

//

// Main program:

// Create IHttpClientFactory instance.
var client =
    new ServiceCollection() // Create DI container.
        .AddHttpClient() // Add HttpClient to DI container.
        .BuildServiceProvider() // Build DI container.
        .GetService<IHttpClientFactory>(); // Get IHttpClientFactory from DI container.

// Create HttpApi instance.
var httpApi = new HttpApi(scheme: Misskey.Net.Uri.Scheme.Https, host: host, client: client);

// Get stats of Misskey instance.
var stats = new Stats(await httpApi.RequestApiAsync(new[] { "stats" }));

Console.WriteLine("-- stats --");

Console.WriteLine($"notes: {stats.NotesCount}");

Console.WriteLine($"users: {stats.UsersCount}");

Console.WriteLine($"instances: {stats.Instances}");

// 

// Authorize this app to Misskey instance with permission `read:notification`.
await httpApi.AuthorizeAsync(
    name: appName,
    permissions: new[] { Permission.NewRead(new PermissionKind.Notifications()) }
);

// Wait for authorization.
var auth = await httpApi.WaitCheckAsync();

if (auth == false)
    throw new Exception("authorization failed");

Console.WriteLine("authorization succeeded");

// 

// Create StreamingApi instance.
using var streamingApi = new StreamingApi(httpApi: httpApi, webSocket: new ClientWebSocket());

// Connect to Misskey instance.
await streamingApi.ConnectStreamingAsync();

Console.WriteLine("connected");

// Connect to local timeline.
var _channelConnection = await streamingApi.ConnectChannelAsync(new Misskey.Net.StreamingApi.Channel.LocalTimeline());

Console.WriteLine("connected to the local timeline channel");

// Connect to main channel.
var _channelConnection2 = await streamingApi.ConnectChannelAsync(new Misskey.Net.StreamingApi.Channel.Main());

Console.WriteLine("connected to the main channel");

while (true)
{
    Console.WriteLine("subscribing...");
    // Subscribe to global timeline.
    var result = await streamingApi.ReceiveAsync();

    switch (result)
    {
        // A note or a renote is posted.
        case StreamMessage.Channel channelMessage:
            var body = channelMessage.body.Body;

            switch (body)
            {
                case ChannelMessageBody.Note noteMessage:
                    var note = noteMessage.body;

                    var user = note.User;

                    // if user.Name is some then name = user.Name else name = "<no name>"
                    var name = FSharpOption<string>.get_IsSome(user.Name) ? user.Name.Value : "<no name>";
                    var text = FSharpOption<string>.get_IsSome(note.Text) ? note.Text.Value : "<image only>"; 

                    if (FSharpOption<Note>.get_IsNone(note.Renote))
                    {
                        Console.WriteLine("-- note --");
                        Console.WriteLine($"user: {name}");
                        Console.WriteLine($"text: {text}");
                    }
                    else
                    {
                        var renote = note.Renote.Value;
                        var original = FSharpOption<string>.get_IsSome(renote.User.Name) ? renote.User.Name.Value : "<no name>";
                        var renotedText = FSharpOption<string>.get_IsSome(renote.Text) ? renote.Text.Value : "<image only>";

                        Console.WriteLine("-- renote --");
                        Console.WriteLine($"user: {original}");
                        Console.WriteLine($"renoted by: {name}");
                        Console.WriteLine($"text: {renotedText}");
                    }
                    break;
                case ChannelMessageBody.Notification notificationMessage:
                    var notification = notificationMessage.body;
                    switch (notification.Body)
                    {
                        case NotificationModule.Body.OfReaction reactionBody:
                            var reaction = reactionBody.Item;
                            var reactingUser = FSharpOption<string>.get_IsSome(reaction.User.Name) ? reaction.User.Name.Value : "<no name>";
                            var reactedText = FSharpOption<string>.get_IsSome(reaction.Note.Text) ? reaction.Note.Text.Value : "<image only>";
                            Console.WriteLine("-- reaction --");
                            Console.WriteLine($"user: {reactingUser}");
                            Console.WriteLine($"reaction: {reaction.Reaction}");
                            Console.WriteLine($"text: {reactedText}");
                            break;
                        default:
                            Console.WriteLine("-- other notification --");
                            Console.WriteLine(notification.Body.ToString());
                            break;
                    }
                    break;
                default:
                    Console.WriteLine($"other message {body}");
                    break;
            }
            break;
        // Connected.
        case StreamMessage.Connected connectedMessage:
            var connected = connectedMessage.body;
            Console.WriteLine("-- connected --");
            Console.WriteLine($"id: {connected.Id}");
            break;
        // Note updated.
        case StreamMessage.NoteUpdated noteUpdatedMessage:
            var noteUpdated = noteUpdatedMessage.body;
            Console.WriteLine("-- note updated --");
            Console.WriteLine($"id: {noteUpdated.Id}");
            var bodyString = noteUpdated.BodyData.ToString().Substring(0, 30) + "...";
            Console.WriteLine($"body: {bodyString}");
            break;
        // Other messages.
        case StreamMessage.Other message:
            Console.WriteLine("-- other message --");
            break;
    }
}

License

MIT

About

High-Level Misskey API Library for .NET

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published