diff --git a/Dockerfile b/Dockerfile index 07b5add6b5eafa150720dfa713fff1b3d62f70c1..e1c50ca7440723d2f5ab7f9c7caa1f00b748528d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,11 @@ RUN apk add \ make \ py-pip +RUN apk add \ + --no-cache \ + --repository https://dl-3.alpinelinux.org/alpine/edge/testing/ \ + vips-dev + # install application dependencies COPY package.json yarn.lock ./ RUN JOBS=max yarn install --non-interactive --frozen-lockfile @@ -31,7 +36,7 @@ WORKDIR /app RUN apk add \ --no-cache \ --repository https://alpine.global.ssl.fastly.net/alpine/v3.10/community \ - fftw + fftw vips COPY --from=build-stage /app/config config COPY --from=build-stage /app/lib lib COPY --from=build-stage /app/node_modules node_modules diff --git a/config/default.toml b/config/default.toml index ff877a0279a515cba20a193434dc99b87f0f6413..e182686d2b09d85efef69d2759a20f6aa78951e0 100644 --- a/config/default.toml +++ b/config/default.toml @@ -20,6 +20,8 @@ service_url = 'http://localhost:8800' # default user avatar, should be a png minimum 512x512 default_avatar = 'https://images.hive.blog/DQmb2HNSGKN3pakguJ4ChCRjgkVuDN9WniFRPmrxoJ4sjR4' +# default user cover, should be a png minimum 1344x240 +default_cover = 'https://images.ecency.com/DQmdA9wjRyGmDwrTza59yLSjCWMEc9sFD5sC4ZU5kL9UMqo/cover_fallback_day.png' # log level to output at log_level = 'debug' diff --git a/src/cover.ts b/src/cover.ts new file mode 100644 index 0000000000000000000000000000000000000000..1af4b367f64ad1366ac29cf7cffbf1aec7cc8c02 --- /dev/null +++ b/src/cover.ts @@ -0,0 +1,64 @@ +/** Serve user cover images. */ + +import * as config from 'config' +import { base58Enc } from './utils' + +import { Account } from '@hiveio/dhive' +import {KoaContext, rpcClient} from './common' +import {APIError} from './error' + +const DefaultCover = config.get('default_cover') as string +const sizeW = 1344 +const sizeH = 240 + +export async function coverHandler(ctx: KoaContext) { + ctx.tag({handler: 'cover'}) + + APIError.assert(ctx.method === 'GET', APIError.Code.InvalidMethod) + APIError.assertParams(ctx.params, ['username']) + + const username = ctx.params['username'] + + interface ExtendedAccount extends Account { + posting_json_metadata?: string + } + + const [account]: ExtendedAccount[] = await rpcClient.database.getAccounts([username]) + + APIError.assert(account, APIError.Code.NoSuchAccount) + + let metadata + + // read from `posting_json_metadata` if version flag is set + if (account.posting_json_metadata) { + try { + metadata = JSON.parse(account.posting_json_metadata) + if (!metadata.profile || !metadata.profile.version) { + metadata = {} + } + } catch (error) { + ctx.log.debug(error, 'unable to parse json_metadata for %s', account.name) + metadata = {} + } + } + + // otherwise, fall back to reading from `json_metadata` + if (!metadata || !metadata.profile) { + try { + metadata = JSON.parse(account.json_metadata) + } catch (error) { + ctx.log.debug(error, 'unable to parse json_metadata for %s', account.name) + metadata = {} + } + } + + let coverUrl: string = DefaultCover + if (metadata.profile && + metadata.profile.cover_image && + metadata.profile.cover_image.match(/^https?:\/\//)) { + coverUrl = metadata.profile.cover_image + } + + ctx.set('Cache-Control', 'public,max-age=600') + ctx.redirect(`/p/${ base58Enc(coverUrl) }?width=${ sizeW }&height=${ sizeH }`) +} diff --git a/src/proxy.ts b/src/proxy.ts index 19beba45091ecfb57d1a41ddb3070ed67a0566fe..e3922174525caad064597a2c8d88ec2e5fed1f21 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -242,7 +242,21 @@ export async function proxyHandler(ctx: KoaContext) { user_agent: 'HiveProxy/1.0 (+https://gitlab.syncad.com/hive/imagehoster)', } as any) } catch (cause) { - throw new APIError({cause, code: APIError.Code.UpstreamError}) + // old or non existing images, try to get from steemitimages server + try { + ctx.log.debug({url: url.toString()}, 'fetching from steemit server') + res = await fetchUrl(`https://steemitimages.com/0x0/${url.toString()}`, { + open_timeout: 5 * 1000, + response_timeout: 5 * 1000, + read_timeout: 60 * 1000, + compressed: true, + parse_response: false, + follow_max: 5, + user_agent: 'SteemitProxy/1.0 (+https://github.com/steemit/imagehoster)', + } as any) + } catch (cause) { + throw new APIError({cause, code: APIError.Code.UpstreamError}) + } } APIError.assert(res.bytes <= MAX_IMAGE_SIZE, APIError.Code.PayloadTooLarge) diff --git a/src/routes.ts b/src/routes.ts index fa5853a80541e5a40e5568abdf4f84be841d0ee9..48d940ede2dfe2ad67b6f3445c84bded3626fb37 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -3,6 +3,7 @@ import * as Router from 'koa-router' import {avatarHandler} from './avatar' +import {coverHandler} from './cover' import {KoaContext} from './common' import {legacyProxyHandler} from './legacy-proxy' import {proxyHandler} from './proxy' @@ -22,6 +23,7 @@ async function healthcheck(ctx: KoaContext) { router.get('/', healthcheck as any) router.get('/.well-known/healthcheck.json', healthcheck as any) router.get('/u/:username/avatar/:size?', avatarHandler as any) +router.get('/u/:username/cover', coverHandler as any) router.post('/hs/:accesstoken', uploadHsHandler as any) router.post('/:username/:signature', uploadHandler as any) router.post('/cs/:username/:signature', uploadCsHandler as any)