Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches: ["main", "develop"]
pull_request:
branches: ["main"]
workflow_dispatch:

jobs:
test:
Expand Down
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@
"name": "S3Plugin",
"dependencies": [],
"config": {}
},
{
"name": "traQAPI",
"dependencies": [],
"config": {}
}
],
"custom_config": {
Expand Down
5 changes: 5 additions & 0 deletions config.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"name": "S3Plugin",
"dependencies": [],
"config": {}
},
{
"name": "traQAPI",
"dependencies": [],
"config": {}
}
],
"redis_clients": [
Expand Down
40 changes: 40 additions & 0 deletions controllers/traq_api.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "traq_api.h"
#include <json/json.h>
#include "../plugins/traQAPI.h"

drogon::Task<drogon::HttpResponsePtr> traq_api::fetchAllStamps([[maybe_unused]] HttpRequestPtr req) {
auto* traqAPI = drogon::app().getPlugin<traQAPI>();
if (!traqAPI) {
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody("traQAPI plugin is not initialized");
co_return response;
}
auto result = co_await traqAPI->fetchAllStamps();
if (!result.has_value()) {
co_return result.error();
}
co_return drogon::HttpResponse::newHttpJsonResponse(std::move(result.value()));
}

drogon::Task<drogon::HttpResponsePtr> traq_api::getStampImage([[maybe_unused]] HttpRequestPtr req, std::string id) {
auto* traqAPI = drogon::app().getPlugin<traQAPI>();
if (!traqAPI) {
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody("traQAPI plugin is not initialized");
co_return response;
}
auto result = co_await traqAPI->getStampImage(id);
if (!result.has_value()) {
co_return result.error();
}
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k200OK);
resp->setContentTypeCode(drogon::CT_CUSTOM);
resp->setContentTypeString(result.value().second);
resp->setBody(std::move(result.value().first));
co_return resp;
}
16 changes: 16 additions & 0 deletions controllers/traq_api.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <drogon/HttpController.h>

using namespace drogon;

class traq_api : public drogon::HttpController<traq_api>
{
public:
METHOD_LIST_BEGIN;
ADD_METHOD_TO(traq_api::fetchAllStamps, "/traq-api/stamps", Get, "AuthFilter");
ADD_METHOD_TO(traq_api::getStampImage, "/traq-api/stamps/{1}/image", Get, "AuthFilter");
METHOD_LIST_END;
drogon::Task<drogon::HttpResponsePtr> fetchAllStamps(HttpRequestPtr req);
drogon::Task<drogon::HttpResponsePtr> getStampImage(HttpRequestPtr req, std::string id);
};
2 changes: 2 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ services:
- MODAL_HOST=http://127.0.0.1:9999
- YOUTUBE_API_KEY=${YOUTUBE_API_KEY:-test-key}
- YOUTUBE_API_BASE_URL=http://127.0.0.1:9999
- TRAQ_TOKEN=${TRAQ_TOKEN:-test-token}
- TRAQ_API_URL=http://127.0.0.1:9999
depends_on:
test-db:
condition: service_healthy
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ services:
BACKEND_URL: ${BACKEND_URL:-http://backend:8080}
LOCAL_WORKER_EXECUTABLE: ${LOCAL_WORKER_EXECUTABLE:-./build/playbacq_worker}
YOUTUBE_API_KEY: ${YOUTUBE_API_KEY:-}
TRAQ_TOKEN: ${TRAQ_TOKEN:-}
TRAQ_API_URL: ${TRAQ_API_URL:-https://q.trap.jp/api/v3}
depends_on:
db:
condition: service_healthy
Expand Down
63 changes: 63 additions & 0 deletions docs/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,69 @@ paths:
description: "アクセスが拒否されました。"
"404":
description: "ユーザーが見つかりませんでした。"
/traq-api/stamps:
get:
summary: "スタンプリストの取得"
description: "スタンプのリストを取得します。"
operationId: getTraqStamps
x-tested: false
tags:
- stamp
responses:
"200":
description: "スタンプリストを返します。"
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
example: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
description: "スタンプUUID"
name:
type: string
example: "thanks-nya"
pattern: "^[a-zA-Z0-9_-]{1,32}$"
description: "スタンプの名前"
"403":
description: "アクセスが拒否されました。"
"500":
description: "サーバー内部エラー"
/traq-api/stamps/{stampId}/image:
get:
summary: "スタンプ画像を取得"
description: "指定したIDのスタンプ画像を返します。"
operationId: getTraqStampImage
x-tested: false
tags:
- stamp
parameters:
- name: stampId
in: path
required: true
schema:
type: string
format: uuid
description: "スタンプUUID"
responses:
"200":
description: "スタンプ画像を返します。"
content:
image/png:
schema:
type: string
format: binary
description: "スタンプ画像のバイナリデータ"
"403":
description: "アクセスが拒否されました。"
"404":
description: "スタンプが見つかりませんでした。"
"500":
description: "サーバー内部エラー"
components:
schemas:
Video:
Expand Down
139 changes: 139 additions & 0 deletions plugins/traQAPI.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "traQAPI.h"

using namespace drogon;

void traQAPI::initAndStart(const Json::Value& config)
{
if (const char* token = std::getenv("TRAQ_TOKEN"); token != nullptr) {
Token = token;
}
if (const char* apiUrl = std::getenv("TRAQ_API_URL"); apiUrl != nullptr) {
ApiUrl = apiUrl;
}
if (Token.empty() || ApiUrl.empty()) {
std::cerr << "TRAQ_TOKEN or TRAQ_API_URL is not set. traQAPI plugin will not be initialized." << std::endl;
return;
}
client = drogon::HttpClient::newHttpClient(ApiUrl);
}

drogon::Task<drogon::HttpResponsePtr> traQAPI::getFromTraQAPI(const std::string& endpoint)
{
if (Token.empty()) {
std::cerr << "TRAQ_TOKEN is not set. Cannot make request to traQ API." << std::endl;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k500InternalServerError);
resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
resp->setBody("traQ Token is not set");
co_return resp;
}
try {
auto req = drogon::HttpRequest::newHttpRequest();
req->setMethod(drogon::Get);
req->setPath(endpoint);
req->addHeader("Authorization", std::format("Bearer {}", Token));
auto resp = co_await client->sendRequestCoro(req);

if (resp->getStatusCode() != drogon::k200OK) {
std::cerr << "Error fetching stamps: " << resp->getBody() << std::endl;
auto errorResp = drogon::HttpResponse::newHttpResponse();
errorResp->setStatusCode(drogon::k500InternalServerError);
errorResp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
errorResp->setBody(std::format("Error fetching stamps: {}", resp->getBody()));
co_return errorResp;
}
co_return resp;
} catch (const std::exception& e) {
std::cerr << "Error fetching stamps: " << e.what() << std::endl;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k500InternalServerError);
resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
resp->setBody(std::string("Error fetching stamps: ") + e.what());
co_return resp;
}
}

drogon::Task<std::expected<Json::Value, drogon::HttpResponsePtr>> traQAPI::fetchAllStamps()
{
{
// キャッシュが有効であればキャッシュを返す
std::shared_lock lock(mutex);
if (!allStampsCache.stamps.empty() && std::chrono::steady_clock::now() - allStampsCache.timestamp < cacheDuration) {
co_return allStampsCache.stamps;
}
}
auto resp = co_await getFromTraQAPI("/api/v3/stamps");
if (resp->getStatusCode() != drogon::k200OK) {
std::cerr << "Error fetching stamps: " << resp->getBody() << std::endl;
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody(std::format("Error fetching stamps: {}", resp->getBody()));
co_return std::unexpected(response);
}

auto jsonPtr = resp->getJsonObject();
if (!jsonPtr) {
std::cerr << "Error parsing JSON response from traQ API" << std::endl;
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody("Error parsing JSON response from traQ API");
co_return std::unexpected(response);
}
if (!jsonPtr->isArray()) {
std::cerr << "Unexpected JSON format from traQ API" << std::endl;
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody("Unexpected JSON format from traQ API");
co_return std::unexpected(response);
}
Json::Value result_array(Json::arrayValue);
for (const auto& stamp : *jsonPtr) {
Json::Value stamp_info;
stamp_info["id"] = stamp.get("id", "").asString();
stamp_info["name"] = stamp.get("name", "").asString();
result_array.append(std::move(stamp_info));
}
// キャッシュに保存
std::unique_lock lock(mutex);
allStampsCache = { result_array, std::chrono::steady_clock::now() };
co_return result_array;
}

drogon::Task<std::expected<std::pair<std::string, std::string>, drogon::HttpResponsePtr>> traQAPI::getStampImage(std::string id)
{
{
// キャッシュが有効であればキャッシュを返す
std::shared_lock lock(mutex);
auto it = stampImageCache.find(id);
if (it != stampImageCache.end() && std::chrono::steady_clock::now() - it->second.timestamp < cacheDuration) {
co_return std::make_pair(it->second.imageRawData, it->second.contentType);
}
}

auto resp = co_await getFromTraQAPI("/api/v3/stamps/" + id + "/image");
if (resp->getStatusCode() != drogon::k200OK) {
std::cerr << "Error fetching stamp image: " << resp->getBody() << std::endl;
auto response = drogon::HttpResponse::newHttpResponse();
response->setStatusCode(drogon::k500InternalServerError);
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
response->setBody(std::format("Error fetching stamp image: {}", resp->getBody()));
co_return std::unexpected(response);
}
std::string imageData = std::string(resp->getBody());
// キャッシュに保存
std::unique_lock lock(mutex);
std::string contentType = resp->getHeader("Content-Type");
if (contentType.empty()) {
contentType = "image/png";
}
stampImageCache[id] = { imageData, contentType, std::chrono::steady_clock::now() };
co_return std::make_pair(imageData, contentType);
}

void traQAPI::shutdown()
{
/// Shutdown the plugin
}
47 changes: 47 additions & 0 deletions plugins/traQAPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <drogon/plugins/Plugin.h>
#include <drogon/HttpController.h>
#include <drogon/HttpClient.h>
#include <expected>
#include <shared_mutex>
#include <chrono>

struct allStampsCacheEntry {
Json::Value stamps;
std::chrono::steady_clock::time_point timestamp;
};

struct stampImageCacheEntry {
std::string imageRawData;
std::string contentType;
std::chrono::steady_clock::time_point timestamp;
};

class traQAPI : public drogon::Plugin<traQAPI>
{
private:
// キャッシュの有効期限は2週間とする
const std::chrono::weeks cacheDuration{ 2 };

std::string Token;
std::string ApiUrl;
drogon::HttpClientPtr client;
std::shared_mutex mutex;
allStampsCacheEntry allStampsCache;
std::unordered_map<std::string, stampImageCacheEntry> stampImageCache;

drogon::Task<drogon::HttpResponsePtr> getFromTraQAPI(const std::string& endpoint);
public:
traQAPI() {}
/// This method must be called by drogon to initialize and start the plugin.
/// It must be implemented by the user.
void initAndStart(const Json::Value& config) override;

drogon::Task<std::expected<Json::Value, drogon::HttpResponsePtr>> fetchAllStamps();
drogon::Task<std::expected<std::pair<std::string, std::string>, drogon::HttpResponsePtr>> getStampImage(std::string id);
/// This method must be called by drogon to shutdown the plugin.
/// It must be implemented by the user.
void shutdown() override;
};

5 changes: 5 additions & 0 deletions test/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@
"name": "S3Plugin",
"dependencies": [],
"config": {}
},
{
"name": "traQAPI",
"dependencies": [],
"config": {}
}
],
"redis_clients": [
Expand Down
Loading
Loading