用 CF-Workers 制作 Steam 签名档
Contents
前情提要
这倒是没什么前情提要,本来之前做的 Github Actions 版的签名档就是利用 CloudFlare Workers 失败的产物。
主要原因还是没弄明白怎么用 js 生成图片,搜索的结果多半都是用 canvas 或者 node.js 的库,但似乎在 Workers 上不太适用。
正好之前看到有人弄了 svg 版的签名档,我才反应过来可以直接用 svg 实现图片,所以才有了这篇。
开搞
准备工作
既然要输出内容,那么肯定要先获取内容,所以先找一下相关的 api 地址。
var userUrl = `https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${apikey}&steamids=${steamid}`;
var lvlUrl = `https://api.steampowered.com/IPlayerService/GetBadges/v0001/?key=${apikey}&steamid=${steamid}&format=json`;
var recentUrl = `https://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=${apikey}&steamid=${steamid}&format=json`;
值得一提的是第二条的 lvlUrl,直接获取等级的那个 API 有时候会读取不到等级,但这个不会。
开头
既然是 Workers,那么肯定是熟悉的开头。
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
})
async function handleRequest(request) {
var reqUrl = new URL(request.url);
var pathname = reqUrl.pathname.split('/');
var params = reqUrl.searchParams;
}
获取 steamid
如果不想让别人用可以写死,想让别人用的话还是要获取一下输入的内容的。
var apikey = 'enter api key here.';
var steamid = pathname[1] ? pathname[1] : '0';
如果不用地址,而是用传参,即 ?steamid=1234567 的形式可以写成这样
var steamid = params.get('steamid');
通过 API 获取内容
访问 API 需要的所有内容都有了,之后通过 fetch 函数获取内容。(asnyc/await 真的省事儿)
这里独立出来一个函数专门处理 API 返回的内容。
async function getSteamData(steamid, apikey) {
var outInfo = {}, res, data, recentNum = 3;
var userUrl = `https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${apikey}&steamids=${steamid}`;
res = await fetch(userUrl);
data = await res.json();
outInfo['username'] = data['response']['players'][0]['personaname'];
outInfo['avatar'] = await getWebImage(data['response']['players'][0]['avatarfull']);
var lvlUrl = `https://api.steampowered.com/IPlayerService/GetBadges/v0001/?key=${apikey}&steamid=${steamid}&format=json`;
res = await fetch(lvlUrl);
data = await res.json();
outInfo['level'] = data['response']['player_level'];
outInfo['gameNum'] = 0;
data['response']['badges'].forEach(function (badge) {
if (badge['badgeid'] == 13) {
outInfo['gameNum'] = badge['level'];
return;
}
});
var recentUrl = `https://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=${apikey}&steamid=${steamid}&format=json`;
res = await fetch(recentUrl);
data = await res.json();
outInfo['recent'] = [];
if (recentNum > data['response']['total_count']) {
recentNum = data['response']['total_count'];
}
for (var i = 0; i < recentNum; i++) {
outInfo['recent'].push(await getWebImage(`https://steamcdn-a.akamaihd.net/steam/apps/${data['response']['games'][i]['appid']}/header.jpg`));
}
return outInfo;
}
获取游戏数用的是 id 为 13 的徽章等级,也就是能加一的游戏数量,比直接从等级接口拿到的数据更准确。
本来图片是打算直接用链接,本地尝试也正常,但是被引用之后好像就不能正常显示了,所以图片用了一个 getWebImage,将图片转换为 base64 的格式输出。
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
async function getWebImage(url) {
var res, data;
res = await fetch(url);
data = await res.arrayBuffer();
return 'data:image/jpeg;base64,'+arrayBufferToBase64(data);
}
输出为 svg
其实这个步骤在实际操作上是第一步,但在逻辑上因为是输出,所以放在了后面。
svg 基本上都是字符串,所以也没什么好说的,画好之后直接复制粘贴组装一下就好。
字体部分引用了 fonts.css 库的 css 代码。
var outInfo = await getSteamData(steamid, apikey);
var svgdata = `<svg width="361" height="144" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><style>.simsun {font-family:Consolas,"Nimbus Roman No9 L","Songti SC","Noto Serif CJK SC","Source Han Serif SC","Source Han Serif CN",STSong,"AR PL New Sung","AR PL SungtiL GB",NSimSun,SimSun,"TW\-Sung","WenQuanYi Bitmap Song","AR PL UMing CN","AR PL UMing HK","AR PL UMing TW","AR PL UMing TW MBE",PMingLiU,MingLiU,serif;} .simkai {font-family:Baskerville,Consolas,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\-Kai",serif;}</style><rect width="100%" height="100%" rx="5" fill="#33415B"/><g fill="white"><image height="64" width="64" x="10" y="10" xlink:href="${outInfo['avatar']}"></image><text x="84" y="32" width="267" height="24" style="font-size: 24px;" class="simkai">${outInfo['username']}</text><text x="84" y="53" width="267" height="14" style="font-size: 14px;" class="simsun">社区等级 | ${outInfo['level']}</text><text x="84" y="71" width="267" height="14" style="font-size: 14px;" class="simsun">游戏数量 | ${outInfo['gameNum']}</text></g><g>`;
if (outInfo['recent'].length > 0) {svgdata += `<image height="50" width="107" x="10" y="84" xlink:href="${outInfo['recent'][0]}"></image>`;}
if (outInfo['recent'].length > 1) {svgdata += `<image height="50" width="107" x="127" y="84" xlink:href="${outInfo['recent'][1]}"></image>`;}
if (outInfo['recent'].length > 2) {svgdata += `<image height="50" width="107" x="244" y="84" xlink:href="${outInfo['recent'][2]}"></image>`;}
svgdata += `</g></svg>`;
return new Response(svgdata, { status: 200, headers: {'Content-Type':'image/svg+xml; charset=utf-8'} });
使用 KV
但即使按照上面的弄法,每次访问仍然需要访问 3 次 API,时间比较长也没什么太大意义,正好 Workers 新增了 KV,可以当作一个小数据库用。
根据教程绑定一个 KV 命名空间之后,就可以直接用了。
访问的时候读取 KV,如果没有内容就去查询 API。
var outInfo = await stsign.get(steamid);
if (outInfo == null) {
outInfo = await getSteamData(steamid, apikey, 3);
} else {
outInfo = JSON.parse(outInfo);
}
在查询 API 尾部加一段。
await stsign.put(steamid, JSON.stringify(outInfo), {expirationTtl: 43200});
可以直接通过 expirationTtl 参数来控制过期时间,而无需自己写代码控制过期,还是挺不错的。
说在最后
其实倒不是说有什么难度,主要算是填上了以前挖的坑,所以稍稍写了一点。