前情提要

这倒是没什么前情提要,本来之前做的 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 参数来控制过期时间,而无需自己写代码控制过期,还是挺不错的。

说在最后

其实倒不是说有什么难度,主要算是填上了以前挖的坑,所以稍稍写了一点。