gRPC, Spring Boot, Kotlin,MySQLで作ったSNSアプリ(通話、チャット)のAPIサンプル

# どんなアプリのAPIか。
- チャット、通話機能があるSNSアプリのAPIです。
- ユーザー登録、プッシュ通知、ブロック・通報機能などのsnsアプリに必要な一通りのAPIを実装しています。

# システムの構成
- フレームワーク：Spring Boot
- 言語：Kotlin
- API：Protocol Buffersで定義、gRPCで実装。
- DB：MySQL

# コード
https://github.com/yusuke-imagawa/talking-sns-api-sample

## コメント
- gRPC, Protocol Buffersがどんなものかは他に沢山情報があるので省略します。
- 画像ファイルのアップロードはこのサンプルに含みません。
- iOSプッシュ通知の用のp8ファイルや、その他の認証用の文字列は無効なものになっています。

# APIの一覧
## アカウント
- ユーザー登録
- プロフィール編集
- 退会

## ユーザー情報：
- ユーザーのリスト取得
- 指定したidのユーザーを取得

## 通話
- 通話を開始。(電話を掛ける)
- 通話を受ける。
- 通話を切る。
- 通話が継続していることを記録。
- 通話のステータス受信(開始、通話中、終了)

## チャット
- メッセージ送信
- メッセージ受信
- すべてのメッセージを取得

## プッシュ通知
- デバイストークン_iOS / 登録, 更新
- ON/OFF更新

## その他
- ユーザーをブロック
- ユーザーを通報

# Protocol BuffersによるAPI仕様定義
- [account.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/account.proto)
- [block.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/block.proto)
- [calling.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/calling.proto)
- [chat.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/chat.proto)
- [commons.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/commons.proto)
- [push_notify.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/push_notify.proto)
- [report.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/report.proto)
- [user.proto](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/proto/user.proto)

# API(gRPC)のコントローラークラス
https://github.com/yusuke-imagawa/talking-sns-api-sample/tree/master/src/main/kotlin/com/talking/api/grpc

- [AccountGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [AccountRegisterGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountRegisterGrpcService.kt)
- [BlockGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [CallingGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [ChatGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [PushNotifyGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [ReportGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)
- [UserGrpcService](https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt)

# build.gradle: protoファイルから、APIのソースを生成
https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/build.gradle

```
group 'com.talking'

// gRPCのコード生成コマンド: ./gradlew generateProto

// ドキュメント: gRPCの実装
// https://github.com/LogNet/grpc-spring-boot-starter

buildscript {
    ext.kotlin_version = '1.3.61' // Required for Kotlin integration
    ext.spring_boot_version = '2.2.0.RELEASE'
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // Required for Kotlin integration
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" // See https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"

        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.5"
    }
}

apply plugin: 'kotlin' // Required for Kotlin integration
apply plugin: "kotlin-spring" // https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support
apply plugin: 'org.springframework.boot'
apply plugin: 'com.google.protobuf'

def grpcVersion = '1.25.0'

repositories {
    mavenCentral()
}

sourceSets {
    main.kotlin.srcDirs += 'src/main/kotlin'
    main.java.srcDirs += 'src/main/java'
    main.java.srcDirs += 'src/main/generated-proto'
}

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
    compile("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
    // testCompile('org.springframework.boot:spring-boot-starter-test')

    // for web
    compile("org.springframework.boot:spring-boot-starter-web:$spring_boot_version")
    compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")

    // for db
    compile("org.springframework.boot:spring-boot-starter-data-jpa:$spring_boot_version")
    runtime("mysql:mysql-connector-java:8.0.15")

    compile("org.springframework.boot:spring-boot-starter-security:$spring_boot_version")

    // grpc
    compile("io.github.lognet:grpc-spring-boot-starter:3.5.0")
    compile "io.grpc:grpc-api:${grpcVersion}"
    compile "io.netty:netty-codec-http2:4.1.42.Final"
    compile "io.grpc:grpc-core:${grpcVersion}"
    compile "io.grpc:grpc-protobuf:${grpcVersion}"
    compile "io.grpc:grpc-stub:${grpcVersion}"

    // cache
    compile(group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '2.8.1')
    // iOSプッシュ通知
    compile(group: 'com.eatthepath', name: 'pushy', version: '0.13.11')

}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.5.0"
    }

    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
        }
    }

    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    outputSubDir = 'generated-proto'
                }
            }
            task.plugins {
                grpc {
                    outputSubDir = 'generated-proto'
                }
            }
        }
    }

    generatedFilesBaseDir = "$projectDir/src/"
}
```

# ddl
https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/docs/DB/ddl.sql
### テーブル
- ユーザー
- プロフィール画像
- チャット
- 通話履歴
- 通報
- ブロック
- プッシュ通知デバイストークン_iOS

# 工夫したところ
## ログインしているユーザーしか利用できないAPIの認証処理を共通化

AuthInterceptor内でrequestのheader内のtoken,userIdで認証。

https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/interceptor/AuthInterceptor.kt

```
@Component
class AuthInterceptor : ServerInterceptor {

    @Autowired
    private lateinit var userService: UserService

    // 参考ソース:
    // https://github.com/saturnism/grpc-by-example-java/blob/master/metadata-context-example/src/main/java/com/example/grpc/server/JwtServerInterceptor.java

    override fun <ReqT : Any?, RespT : Any?> interceptCall(
        call: ServerCall<ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler<ReqT, RespT>
    ): ServerCall.Listener<ReqT> {

        val userId = headers.get(GrpcConstant.USER_ID_METADATA_KEY)?.toLong() ?:
                throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)

        val apiToken = headers.get(GrpcConstant.API_TOKEN_METADATA_KEY) ?:
                throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)

        val user = userService.findByIdAndToken(userId, apiToken) ?:
            throw StatusRuntimeException(Status.UNAUTHENTICATED, headers)

        val ctx = Context.current().withValue(GrpcConstant.AUTH_USER_CONTEXT_KEY, user)
        return Contexts.interceptCall(ctx, call, headers, next)
    }
}
```

認証処理を利用したいAPIのクラスにAuthInterceptorを設定
https://github.com/yusuke-imagawa/talking-sns-api-sample/blob/master/src/main/kotlin/com/talking/api/grpc/AccountGrpcService.kt

```
@GRpcService(interceptors = [AuthInterceptor::class])
class AccountGrpcService: AccountServiceGrpc.AccountServiceImplBase() {
```