diff --git a/package.json b/package.json index 5526fdab7..315c9ffcc 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@types/portscanner": "2.1.0", "@types/pug": "2.0.4", "@types/qrcode": "1.3.2", + "@types/random-seed": "0.3.3", "@types/ratelimiter": "2.1.28", "@types/redis": "2.8.12", "@types/rename": "1.0.1", @@ -103,6 +104,7 @@ "bootstrap-vue": "2.0.0-rc.13", "bull": "3.7.0", "cafy": "15.1.1", + "canvas": "2.4.1", "chai": "4.2.0", "chalk": "2.4.2", "cli-highlight": "2.1.0", @@ -188,6 +190,7 @@ "pug": "2.0.3", "punycode": "2.1.1", "qrcode": "1.3.3", + "random-seed": "0.3.0", "randomcolor": "0.5.4", "ratelimiter": "3.3.0", "recaptcha-promise": "0.1.3", diff --git a/src/misc/gen-avatar.ts b/src/misc/gen-avatar.ts new file mode 100644 index 000000000..7d22ee98e --- /dev/null +++ b/src/misc/gen-avatar.ts @@ -0,0 +1,89 @@ +/** + * Random avatar generator + */ + +import { createCanvas } from 'canvas'; +import * as gen from 'random-seed'; + +const size = 512; // px +const n = 5; // resolution +const margin = (size / n) / 1.5; +const colors = [ + '#e57373', + '#F06292', + '#BA68C8', + '#9575CD', + '#7986CB', + '#64B5F6', + '#4FC3F7', + '#4DD0E1', + '#4DB6AC', + '#81C784', + '#8BC34A', + '#AFB42B', + '#F57F17', + '#FF5722', + '#795548', + '#455A64', +]; +const bg = '#e9e9e9'; + +const actualSize = size - (margin * 2); +const cellSize = actualSize / n; +const sideN = Math.floor(n / 2); + +/** + * Generate buffer of random avatar by seed + */ +export function genAvatar(seed: string) { + const rand = gen.create(seed); + const canvas = createCanvas(size, size); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = bg; + ctx.beginPath(); + ctx.fillRect(0, 0, size, size); + + ctx.fillStyle = colors[rand(colors.length)]; + + // side bitmap (filled by false) + const side: boolean[][] = new Array(sideN); + for (let i = 0; i < side.length; i++) { + side[i] = new Array(n).fill(false); + } + + // 1*n (filled by false) + const center: boolean[] = new Array(n).fill(false); + + // tslint:disable-next-line:prefer-for-of + for (let x = 0; x < side.length; x++) { + for (let y = 0; y < side[x].length; y++) { + side[x][y] = rand(3) === 0; + } + } + + for (let i = 0; i < center.length; i++) { + center[i] = rand(3) === 0; + } + + // Draw + for (let x = 0; x < n; x++) { + for (let y = 0; y < n; y++) { + const isXCenter = x === ((n - 1) / 2); + if (isXCenter && !center[y]) continue; + + const isLeftSide = x < ((n - 1) / 2); + if (isLeftSide && !side[x][y]) continue; + + const isRightSide = x > ((n - 1) / 2); + if (isRightSide && !side[sideN - (x - sideN)][y]) continue; + + const actualX = margin + (cellSize * x); + const actualY = margin + (cellSize * y); + ctx.beginPath(); + ctx.fillRect(actualX, actualY, cellSize, cellSize); + } + } + + return canvas.toBuffer(); +} diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 83cca2f88..9e4247545 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -3,6 +3,7 @@ import { User, ILocalUser, IRemoteUser } from '../entities/user'; import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; import rap from '@prezzemolo/rap'; import { ensure } from '../../prelude/ensure'; +import config from '../../config'; @EntityRepository(User) export class UserRepository extends Repository { @@ -88,7 +89,7 @@ export class UserRepository extends Repository { name: user.name, username: user.username, host: user.host, - avatarUrl: user.avatarUrl, + avatarUrl: user.avatarUrl ? user.avatarUrl : config.url + '/avatar/' + user.id, avatarColor: user.avatarColor, isAdmin: user.isAdmin || undefined, isBot: user.isBot || undefined, diff --git a/src/server/file/assets/avatar.jpg b/src/server/file/assets/avatar.jpg deleted file mode 100644 index cb7641deb..000000000 --- a/src/server/file/assets/avatar.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d342146d18ab53f17c55310c8f450c796d24d3dde21f39500c61d7c14fe1d61 -size 1261 diff --git a/src/server/file/index.ts b/src/server/file/index.ts index e3487a263..1cdf5207e 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -21,12 +21,6 @@ app.use(async (ctx, next) => { // Init router const router = new Router(); -router.get('/default-avatar.jpg', ctx => { - const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); - ctx.set('Content-Type', 'image/jpeg'); - ctx.body = file; -}); - router.get('/app-default.jpg', ctx => { const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); ctx.set('Content-Type', 'image/jpeg'); diff --git a/src/server/index.ts b/src/server/index.ts index 601e288f3..7d8938d58 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -25,6 +25,7 @@ import Logger from '../services/logger'; import { program } from '../argv'; import { UserProfiles } from '../models'; import { networkChart } from '../services/chart'; +import { genAvatar } from '../misc/gen-avatar'; export const serverLogger = new Logger('server', 'gray', false); @@ -72,6 +73,12 @@ router.use(activityPub.routes()); router.use(nodeinfo.routes()); router.use(wellKnown.routes()); +router.get('/avatar/:x', ctx => { + const avatar = genAvatar(ctx.params.x); + ctx.set('Content-Type', 'image/png'); + ctx.body = avatar; +}); + router.get('/verify-email/:code', async ctx => { const profile = await UserProfiles.findOne({ emailVerifyCode: ctx.params.code