Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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=${TRAQ_API_URL:-https://q.trap.jp/api/v3}
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