Compare commits

..

62 commits

Author SHA1 Message Date
c89080ff00
autoformat 2022-01-19 22:00:29 +01:00
f24047b33b
fix canvas version mismatch breaking build 2022-01-19 20:52:02 +01:00
f004daaa3c add docker deno build to Makefile, clean step, fix app build, .gitignore 2021-09-23 12:38:41 +02:00
ff39c8eeb3 bot error reporting does not use colors & truncates at 2500 chars 2021-09-22 23:57:42 +02:00
cf19301f96 add 60s timeout to bot commands to prevent lockups 2021-09-22 23:57:06 +02:00
a5068e4b64 fix bot --rr flag; tidy up bot /help & /start; cosmetics 2021-09-22 23:56:42 +02:00
04ba5b7e4e show used images in CLI and bot; caption cmdline in bot 2021-09-22 23:55:48 +02:00
546102205e fix recursive defaults 2021-09-20 23:06:11 +02:00
fd06a2a618 add recursive args to bot as well 2021-09-20 23:05:24 +02:00
e391b9b6bf add cli params for recursive modes 2021-09-20 22:56:02 +02:00
bfe739a487 recursive traversal is implicit 2021-09-20 22:43:58 +02:00
85881f877c cli - reformat & lint 2021-09-20 22:41:00 +02:00
4d8b0b85dd cli - use proper defaults 2021-09-20 22:40:12 +02:00
33603eabff cli - allow multiple modes to be selected, recursive collages
also use module augmentation to simplify types and all

TODO: make web app also use the same recursive code
2021-09-20 22:38:04 +02:00
2647238410 check both cases for directories 2021-09-19 13:47:52 +02:00
405fd3cad5 checks for nonexistent dirs 2021-09-19 13:44:35 +02:00
9a13c3c393 ...and -n 2021-09-19 13:36:31 +02:00
1e1faaa4c9 fix width/height args 2021-09-19 13:34:29 +02:00
0edc9a7ab4 fix args 2021-09-19 13:32:50 +02:00
8915a36b3d actual cmdline parsing in tg 2021-09-19 13:19:00 +02:00
d443fc6d7e fix persistence (again) 2021-09-19 12:21:20 +02:00
6091c19c8b update gitignore 2021-09-19 12:19:31 +02:00
b74d5838eb fix persistence 2021-09-19 12:18:32 +02:00
918e021eed fix lint 2021-09-19 12:14:41 +02:00
8ea2782038 remove clamping down files, improve logging 2021-09-19 12:09:17 +02:00
7e41fe054e autoformat & reasonable /start command 2021-09-19 12:09:15 +02:00
7e6a761363 lazy load images in deno 2021-09-19 12:08:46 +02:00
7919e4fb7d makefile improvements 2021-09-19 11:30:28 +02:00
e82fcd5d42 error logging 2021-09-19 11:30:17 +02:00
c89777621a unstable in .gitlab-ci.yml 2021-09-19 10:34:09 +02:00
3409978203 rudimentary tg bot 2021-09-19 10:31:34 +02:00
d8dff75411 deno build --unstable 2021-09-19 09:48:25 +02:00
944dc9ca5f add a --recursive option to deno cli 2021-09-18 19:04:29 +02:00
26ceccbde2 formatting 2021-09-18 18:58:16 +02:00
e34c3a96c4 pin specific canvas version in deno 2021-09-18 18:57:38 +02:00
86c9644972 -m param without args list modes 2021-09-18 12:11:03 +02:00
e9bc9f568d limit cli only to jpgs/pngs (by default) 2021-09-18 11:42:44 +02:00
eb455e2311 clamp cli to 25 images for performance reasons 2021-09-17 19:09:51 +02:00
57f32ef643 cli takes directories as well as files 2021-09-17 18:56:57 +02:00
cd76a36d99 adjust .gitlab-ci.yml to also build app, remove Makefile dep 2021-09-15 21:29:48 +02:00
71af6bb680 add base CI for Deno 2021-09-15 21:22:18 +02:00
6720eace7a add Makefile 2021-09-15 21:19:22 +02:00
0982857082 fix linter errors, reformat 2021-09-15 21:19:03 +02:00
1ba396bc7c make image loading work even in compiled version (?) 2021-09-15 21:16:01 +02:00
a656d37d8c reformat cli/main.ts 2021-09-15 21:15:41 +02:00
14efcf03a1 unnecessary files, fix default extension 2021-09-15 20:56:40 +02:00
b5a6b70c2b npx browserslist@latest --update-db 2021-09-15 19:14:58 +02:00
5734774366 autoformat, fix lint errors 2021-09-15 19:13:26 +02:00
054f9a9218 deno cli version (ugly) proof of concept 2021-09-15 19:09:44 +02:00
79f9108f34 autoformat 2021-09-11 23:51:22 +02:00
de53e1b741 add CHANGELOG.md 2020-07-20 15:11:22 +02:00
d319de86d9 version bump 2020-07-20 14:57:14 +02:00
f54b61e236 add exclusion for shuffle & recursive modes 2020-07-20 14:57:05 +02:00
c481d5739f version bump proper 2020-07-17 19:57:46 +02:00
89091dcd24 version bump 2020-07-17 19:14:52 +02:00
64fa7098a0 add recursion 2020-07-17 19:14:42 +02:00
7cf393af71 major refactor (add segments) in preparation for recursive collages 2020-07-17 16:11:40 +02:00
3866522404 version bump 2020-07-17 12:00:42 +02:00
5473d0c46a fix blending context 2020-07-17 12:00:33 +02:00
ebb7bc537f version bump 2020-07-17 11:57:22 +02:00
4b102137f7 add blend mode, clear canvas before redraw, various improvements 2020-07-17 11:57:11 +02:00
1c3b9c7c46 Add LICENSE 2020-07-16 15:46:43 +00:00
25 changed files with 1871 additions and 310 deletions

4
.gitignore vendored
View file

@ -1,5 +1,6 @@
.DS_Store .DS_Store
node_modules node_modules
.npm
/dist /dist
# local env files # local env files
@ -20,3 +21,6 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Build files
/kollagen

34
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,34 @@
stages:
- lint
- build
deno_lint:
stage: lint
image: denoland/deno:latest
script:
- cd cli
- deno lint
deno_build:
stage: build
image: denoland/deno:latest
script:
- cd cli
- deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
artifacts:
paths:
- kollagen
app_build:
stage: build
image: node:lts
script:
- node --version && npm --version
- make app
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm
artifacts:
paths:
- dist

49
CHANGELOG.md Normal file
View file

@ -0,0 +1,49 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.0] - 2020-07-20
### Added
- Specific modes can now be excluded from rotation in shuffle & recursive modes
## [0.1.0] - 2020-07-17
### Added
- Recursive collage placement mode!
### Changed
- Refactored modes - they are no longer solely responsible for drawing, but can use common rendering functions through composition.
### Fixed
- Incorrect size calculation for "Irregular column" mode.
## [0.0.4] - 2020-07-17
### Fixed
- Blending mode now properly resets after using the "Blend" collage mode.
## [0.0.3] - 2020-07-17
### Added
- "Blend" collage mode - based on a random selection of compositing operations.
- LICENSE file.
## [0.0.2] - 2020-07-16
### Changed
- Split "Grid" mode into "Clean grid" and "Irregular grid" (because "clean cropping" config only really applied to this mode.)
## [0.0.1] - 2020-07-15
### Added
- First public version!
- Basic modes: Grid, (ir)regular row, (ir)regular column, concentric modes.
[Unreleased]: https://gitlab.com/tmladek/kollagen/-/compare/v0.2.0...master
[0.2.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.1.0...v0.2.0
[0.1.0]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.4...v0.1.0
[0.0.4]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.3...v0.0.4
[0.0.3]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.2...v0.0.3
[0.0.2]: https://gitlab.com/tmladek/kollagen/-/compare/v0.0.1...v0.0.2
[0.0.1]: https://gitlab.com/tmladek/kollagen/-/tags/v0.0.1

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Tomáš Mládek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
Makefile Normal file
View file

@ -0,0 +1,21 @@
all: clean deno_lint deno_docker tgbot app
deno_lint:
cd cli && deno lint
deno: ./kollagen
./kollagen:
cd cli && deno compile --import-map ./import_map.json --allow-read --allow-write --unstable -o ../kollagen main.ts
deno_docker:
docker run --rm -v $$PWD:/app denoland/deno compile --import-map /app/cli/import_map.json --allow-read --allow-write --unstable -o /app/kollagen /app/cli/main.ts
app:
npm ci --cache .npm --prefer-offline && npm run build
tgbot: ./kollagen
docker build -t kollagen-bot -f tgbot/Dockerfile .
clean:
rm -rvf dist kollagen

96
cli/collages.ts Normal file
View file

@ -0,0 +1,96 @@
import { CollageModes } from "../src/common/collages.ts";
import {
init,
CanvasRenderingContext2D,
EmulatedCanvas2D,
Image,
} from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import { CollageImage } from "../src/common/types.ts";
const canvasKit = await init();
export class ProxyImage implements CollageImage {
private filepath: string | null;
private _image: Image | undefined;
constructor(input: string | Image) {
if (typeof input === "string") {
this.filepath = input;
} else {
this.filepath = null;
this._image = input;
}
}
public get path(): string | null {
return this.filepath;
}
public get image(): Image {
if (!this._image) {
const image = canvasKit.MakeImageFromEncoded(
Deno.readFileSync(this.filepath!)
);
if (!image) {
throw Error(`Failed loading ${this.filepath}!`);
}
this._image = image;
}
return this._image;
}
public get width(): number {
return this.image.width();
}
public get height(): number {
return this.image.height();
}
}
declare module "https://deno.land/x/canvas@v1.3.0/mod.ts" {
interface HTMLCanvasElement {
width: number;
height: number;
getContext(x: "2d"): CanvasRenderingContext2D;
}
}
export class DenoCollageModes extends CollageModes<
CanvasRenderingContext2D,
ProxyImage,
EmulatedCanvas2D
> {
createCanvas(w: number, h: number): EmulatedCanvas2D {
const canvas = canvasKit.MakeCanvas(Math.round(w), Math.round(h));
if (!canvas) {
throw Error("Error initializing canvas.");
}
return canvas;
}
canvasToImage(canvas: EmulatedCanvas2D): PromiseLike<ProxyImage> {
const image = canvasKit.MakeImageFromEncoded(canvas.toBuffer());
if (!image) {
throw Error("Something went wrong converting canvas to image.");
}
return Promise.resolve(new ProxyImage(image));
}
drawImage(
ctx: CanvasRenderingContext2D,
image: ProxyImage,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void {
ctx.drawImage(image.image, sx, sy, sw, sh, dx, dy, dw, dh);
}
}
export const denoCollageModes = new DenoCollageModes();

6
cli/import_map.json Normal file
View file

@ -0,0 +1,6 @@
{
"imports": {
"@/common/types": "../src/common/types.ts",
"@/common/utils": "../src/common/utils.ts"
}
}

105
cli/main.ts Normal file
View file

@ -0,0 +1,105 @@
import { parse } from "https://deno.land/std@0.106.0/flags/mod.ts";
import { createCanvas } from "https://deno.land/x/canvas@v1.3.0/mod.ts";
import {
collageModeType,
DisplayCollageModeType,
displayCollageModeType,
} from "../src/common/collages.ts";
import { denoCollageModes, ProxyImage } from "./collages.ts";
import { CollageConfig } from "../src/common/types.ts";
import { choice, shuffle } from "../src/common/utils.ts";
import { walkSync } from "https://deno.land/std@0.107.0/fs/mod.ts";
import { parseCollageModes, parseDisplayCollageModes } from "./util.ts";
const args = parse(Deno.args, {
alias: {
w: "width",
h: "height",
o: "output",
m: "mode",
},
boolean: ["rr"],
default: {
w: 640,
h: 640,
include: "*.png, *.jpg",
output: "collage.png",
rl: 2,
rr: false,
},
});
if (args["mode"] === true) {
console.log(displayCollageModeType.join(", "));
Deno.exit(0);
}
const files: Set<string> = new Set();
const includeExtensions = Array.from(
String(args["include"]).matchAll(/\*\.([\w]+)/g)
).map(([_, group]) => group);
args["_"].forEach((arg) => {
arg = arg.toString();
if (Deno.statSync(arg).isDirectory) {
Array.from(
walkSync(arg, {
maxDepth: Infinity,
includeDirs: false,
includeFiles: true,
exts: includeExtensions.length ? includeExtensions : undefined,
})
).forEach((entry) => files.add(entry.path));
} else {
files.add(arg);
}
});
if (files.size < 2) {
console.error("kollagen needs at least 2 images to work.");
Deno.exit(1);
}
const shuffledFiles = shuffle(Array.from(files));
const images: ProxyImage[] = shuffledFiles.map((file) => new ProxyImage(file));
const allModeKeys = args["mode"]
? parseDisplayCollageModes(args["mode"])
: displayCollageModeType;
const canvas = createCanvas(args["width"], args["height"]);
const context = canvas.getContext("2d");
const collageConfig: CollageConfig = {
numImages: args["n"],
};
const modeKey: DisplayCollageModeType = choice(allModeKeys);
if (modeKey === "recursive") {
console.log(
`Creating a recursive collage, choosing from ${shuffledFiles.length} files...`
);
await denoCollageModes.recursiveDraw(context, images, {
modes: args["rm"] ? parseCollageModes(args["rm"]) : collageModeType,
repeat: args["rr"],
level: args["rl"],
});
} else {
const mode = denoCollageModes.modes[modeKey];
console.log(
`Creating a "${mode.name}" collage, choosing from ${shuffledFiles.length} files...`
);
const segments = mode.getSegments(context, collageConfig, images);
mode.place(context, images, segments);
console.log(
`Used: ${images
.slice(0, segments.length)
.map((img) => img.path)
.join(", ")}`
);
}
const output = args["output"];
console.log(`Saving to "${output}"...`);
await Deno.writeFile(output, canvas.toBuffer());

29
cli/util.ts Normal file
View file

@ -0,0 +1,29 @@
import { CollageModeType, DisplayCollageModeType, isCollageModeType, isDisplayCollageModeType } from "../src/common/collages.ts";
export function parseDisplayCollageModes(input: string): DisplayCollageModeType[] {
const result: DisplayCollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isDisplayCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}
export function parseCollageModes(input: string): CollageModeType[] {
const result: CollageModeType[] = [];
input.split(",").map((m) => m.trim()).forEach((m) => {
if (isCollageModeType(m)) {
result.push(m);
} else {
throw Error(`"${m}" is not a valid collage mode.`);
}
});
return result;
}

8
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "kollagen", "name": "kollagen",
"version": "0.1.0", "version": "0.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -2810,9 +2810,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001085", "version": "1.0.30001257",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA==", "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==",
"dev": true "dev": true
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "kollagen", "name": "kollagen",
"version": "0.0.2", "version": "0.2.0",
"homepage": "https://gitlab.com/tmladek/kollagen", "homepage": "https://gitlab.com/tmladek/kollagen",
"private": true, "private": true,
"scripts": { "scripts": {

View file

@ -1,173 +1,14 @@
import {CollageMode} from "@/types"; import { CollageModes } from "./common/collages";
import {randint, shuffle} from "@/utils";
const collageModeType = ["clean_grid", "chaos_grid", "row", "irow", "col", "icol", "concentric_factor", "concentric_spaced"] as const; export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap, any> {
export type CollageModeType = typeof collageModeType[number]; drawImage(ctx: CanvasRenderingContext2D, image: ImageBitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void {
ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
function cleanDraw(ctx: CanvasRenderingContext2D, image: ImageBitmap,
x: number, y: number, w: number, h: number) {
const scaleRatio = Math.max(w / image.width, h / image.height);
ctx.drawImage(
image,
image.width / 2 - w / scaleRatio / 2, image.height / 2 - h / scaleRatio / 2,
w / scaleRatio, h / scaleRatio,
x - w / 2, y - h / 2,
w, h
);
}
function getGridPoints(ctx: CanvasRenderingContext2D, idx: number) {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .25;
break;
case 1:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .25;
break;
case 2:
x = ctx.canvas.width * .25;
y = ctx.canvas.height * .75;
break;
case 3:
x = ctx.canvas.width * .75;
y = ctx.canvas.height * .75;
break;
} }
return [x, y];
}
const modes: { [key in CollageModeType]: CollageMode } = { createCanvas(w: number, h: number) {
"clean_grid": { throw new Error("Method not implemented.");
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
selectedImages.forEach((image, idx) => {
const [x, y] = getGridPoints(ctx, idx);
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"chaos_grid": {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4
},
place: (ctx, images, config) => {
const quadrantSize = [ctx.canvas.width / 2, ctx.canvas.height / 2];
const selectedImages = shuffle(images).slice(0, 4);
shuffle(selectedImages.map((image, idx) => [image, idx] as [ImageBitmap, number]))
.forEach(([image, idx]) => {
const [x, y] = getGridPoints(ctx, idx);
const scaleRatio = Math.max(quadrantSize[0] / image.width, quadrantSize[1] / image.height);
ctx.drawImage(image,
x - (image.width * scaleRatio / 2), y - (image.height * scaleRatio / 2),
image.width * scaleRatio, image.height * scaleRatio);
});
}
},
"row": {
name: "Regular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"irow": {
name: "Irregular Row",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width / selectedImages.length, ctx.canvas.height];
selectedImages.forEach((image, idx) => {
const x = idx * quadrantSize[0] + quadrantSize[0] / 2;
const y = quadrantSize[1] / 2;
const w = Math.min(ctx.canvas.width / 2,
quadrantSize[1] + Math.random() * (quadrantSize[1] - (quadrantSize[1] / image.height) * image.width));
cleanDraw(ctx, image, x, y, w, quadrantSize[1]);
});
}
},
"col": {
name: "Regular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
cleanDraw(ctx, image, x, y, quadrantSize[0], quadrantSize[1]);
});
}
},
"icol": {
name: "Irregular Column",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const quadrantSize = [ctx.canvas.width, ctx.canvas.height / selectedImages.length];
selectedImages.forEach((image, idx) => {
const x = quadrantSize[0] / 2;
const y = idx * quadrantSize[1] + quadrantSize[1] / 2;
const h = Math.min(ctx.canvas.height / 2,
quadrantSize[0] + Math.random() * (quadrantSize[0] - (quadrantSize[0] / image.width) * image.height));
cleanDraw(ctx, image, x, y, quadrantSize[0], h);
});
},
},
"concentric_factor": {
name: "Constant factor concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
let factor: number;
if (Math.random() > .5) {
const factors = [1 / Math.sqrt(2), .5, .88];
factor = factors[Math.floor(Math.random() * factors.length)];
} else {
factor = 1 - (1 / selectedImages.length);
}
selectedImages.forEach((image, idx) => {
const ratio = Math.pow(factor, idx);
cleanDraw(ctx, image, x, y, ctx.canvas.width * ratio, ctx.canvas.height * ratio);
});
}
},
"concentric_spaced": {
name: "Equally spaced concentric",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(4) + 2);
const x = ctx.canvas.width / 2;
const y = ctx.canvas.height / 2;
selectedImages.forEach((image, idx) => {
cleanDraw(
ctx, image, x, y,
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
);
});
}
} }
}; canvasToImage(canvas: any): PromiseLike<ImageBitmap> {
throw new Error("Method not implemented.");
export default modes; }
}

397
src/common/collages.ts Normal file
View file

@ -0,0 +1,397 @@
import {
CollageCanvas,
CollageConfig,
CollageContext,
CollageImage,
CollageMode,
Segment,
} from "@/common/types";
import { choice, randint, range, shuffle } from "@/common/utils";
export const collageModeType = [
"clean_grid",
"chaos_grid",
"row",
"irow",
"col",
"icol",
"concentric_factor",
"concentric_spaced",
"blend",
] as const;
export type CollageModeType = typeof collageModeType[number];
export function isCollageModeType(value: string): value is CollageModeType {
return (collageModeType as readonly string[]).includes(value);
}
export const displayCollageModeType = [
...collageModeType,
"recursive",
] as const;
export type DisplayCollageModeType = typeof displayCollageModeType[number];
export function isDisplayCollageModeType(
value: string
): value is DisplayCollageModeType {
return (displayCollageModeType as readonly string[]).includes(value);
}
export interface RecursiveCollageConfig {
level: number;
repeat: boolean;
modes: readonly CollageModeType[];
}
export abstract class CollageModes<
C extends CollageContext,
I extends CollageImage,
CS extends CollageCanvas
> {
readonly modes: { [key in CollageModeType]: CollageMode<C, I> } = {
clean_grid: {
name: "Clean Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: this.cleanPlace.bind(this),
},
chaos_grid: {
name: "Irregular Grid",
minImages: 4,
forceConfig: {
numImages: 4,
},
getSegments: this.getGridSegments,
place: (ctx, images, segments) => {
const shuffledImages = shuffle(images);
shuffle(
segments.map((segment, idx) => [segment, idx] as [Segment, number])
).forEach(([segment, idx]) => {
const image = shuffledImages[idx];
const scaleRatio = Math.max(
segment.w / image.width,
segment.h / image.height
);
this.drawImage(
ctx,
image,
0,
0,
image.width,
image.height,
segment.x - (image.width * scaleRatio) / 2,
segment.y - (image.height * scaleRatio) / 2,
image.width * scaleRatio,
image.height * scaleRatio
);
});
},
},
row: {
name: "Regular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
irow: {
name: "Irregular Row",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width / numImages, ctx.canvas.height];
return range(numImages).map((idx) => {
const irregularWidth = images
? segmentSize[0] +
Math.random() *
((segmentSize[1] / images[idx].height) * images[idx].width -
segmentSize[0])
: segmentSize[0] + Math.random() * segmentSize[0] * 0.5;
return {
x: idx * segmentSize[0] + segmentSize[0] / 2,
y: segmentSize[1] / 2,
w: Math.min(ctx.canvas.width / 2, irregularWidth),
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
col: {
name: "Regular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: segmentSize[1],
};
});
},
place: this.cleanPlace.bind(this),
},
icol: {
name: "Irregular Column",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
const segmentSize = [ctx.canvas.width, ctx.canvas.height / numImages];
return range(numImages).map((idx) => {
const irregularHeight = images
? segmentSize[1] +
Math.random() *
((segmentSize[0] / images[idx].width) * images[idx].height -
segmentSize[1])
: segmentSize[1] + Math.random() * segmentSize[1] * 0.5;
return {
x: segmentSize[0] / 2,
y: idx * segmentSize[1] + segmentSize[1] / 2,
w: segmentSize[0],
h: Math.min(ctx.canvas.height / 2, irregularHeight),
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_factor: {
name: "Constant factor concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(4) + 2
);
let factor: number;
if (Math.random() > 0.5) {
factor = choice([1 / Math.sqrt(2), 0.5, 0.88]);
} else {
factor = 1 - 1 / numImages;
}
return range(numImages).map((idx) => {
const ratio = Math.pow(factor, idx);
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width * ratio,
h: ctx.canvas.height * ratio,
};
});
},
place: this.cleanPlace.bind(this),
},
concentric_spaced: {
name: "Equally spaced concentric",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((idx) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width - (ctx.canvas.width / numImages) * idx,
h: ctx.canvas.height - (ctx.canvas.height / numImages) * idx,
};
});
},
place: this.cleanPlace.bind(this),
},
blend: {
name: "Blending",
minImages: 2,
getSegments: (ctx, config, images) => {
const numImages = Math.min(
images?.length || Infinity,
config?.numImages || randint(2) + 2
);
return range(numImages).map((_) => {
return {
x: ctx.canvas.width / 2,
y: ctx.canvas.height / 2,
w: ctx.canvas.width,
h: ctx.canvas.height,
};
});
},
place: (ctx, images, segments) => {
ctx.globalCompositeOperation = choice([
"difference",
"saturation",
"soft-light",
"overlay",
]);
this.cleanPlace(ctx, images, segments);
},
},
};
public async recursiveDraw(
context: C,
images: I[],
recursiveConfig: RecursiveCollageConfig
) {
const localImages = images.concat();
const rootSegment: Segment = {
x: 0,
y: 0,
w: context.canvas.width,
h: context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<I> => {
// console.debug(segment, level);
if (segment === rootSegment || level <= recursiveConfig.level - 1) {
const canvas = this.createCanvas(segment.w, segment.h);
const modeKey = choice(recursiveConfig.modes);
// console.debug(modeKey);
const mode = this.modes[modeKey];
const ctx = canvas.getContext("2d") as C;
const segments = mode.getSegments(ctx);
// console.debug(segments);
const bitmaps = await Promise.all(
segments.map((segment: Segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await this.canvasToImage(canvas);
} else {
if (recursiveConfig.repeat) {
return choice(localImages);
} else {
if (localImages.length > 0) {
return localImages.pop() as I;
} else {
throw "RAN OUT OF IMAGES";
}
}
}
};
const result = await processSegment(rootSegment, 0);
this.drawImage(
context,
result,
0,
0,
result.width,
result.height,
0,
0,
context.canvas.width,
context.canvas.height
);
}
private cleanDraw(
ctx: C,
image: I,
x: number,
y: number,
w: number,
h: number
) {
const scaleRatio = Math.max(w / image.width, h / image.height);
this.drawImage(
ctx,
image,
image.width / 2 - w / scaleRatio / 2,
image.height / 2 - h / scaleRatio / 2,
w / scaleRatio,
h / scaleRatio,
x - w / 2,
y - h / 2,
w,
h
);
}
private cleanPlace(ctx: C, images: I[], segments: Segment[]) {
segments.forEach((segment, idx) => {
this.cleanDraw(
ctx,
images[idx],
segment.x,
segment.y,
segment.w,
segment.h
);
});
}
private getGridSegments(ctx: C, config?: CollageConfig) {
return [0, 1, 2, 3].map((idx) => {
let x!: number, y!: number;
switch (idx) {
case 0:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.25;
break;
case 1:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.25;
break;
case 2:
x = ctx.canvas.width * 0.25;
y = ctx.canvas.height * 0.75;
break;
case 3:
x = ctx.canvas.width * 0.75;
y = ctx.canvas.height * 0.75;
break;
}
return {
x,
y,
w: ctx.canvas.width / 2,
h: ctx.canvas.height / 2,
};
});
}
abstract createCanvas(w: number, h: number): CS;
abstract canvasToImage(canvas: CS): PromiseLike<I>;
abstract drawImage(
ctx: C,
image: I,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number
): void;
}

35
src/common/types.ts Normal file
View file

@ -0,0 +1,35 @@
export interface CollageMode<C extends CollageContext, I extends CollageImage> {
name: string;
minImages: number;
getSegments: (ctx: C, config?: CollageConfig, images?: I[]) => Segment[];
place: (ctx: C, images: I[], segments: Segment[]) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}
export interface Segment {
x: number;
y: number;
w: number;
h: number;
}
export interface CollageContext {
globalCompositeOperation: string;
canvas: CollageCanvas
}
export interface CollageCanvas {
width: number;
height: number;
getContext: (x: '2d') => CollageContext | null
}
export interface CollageImage {
width: number;
height: number;
}

View file

@ -17,3 +17,11 @@ export function shuffle<T>(a: T[]): T[] {
export function randint(n: number) { export function randint(n: number) {
return Math.floor(Math.random() * n); return Math.floor(Math.random() * n);
} }
export function choice<T>(arr: readonly T[]): T {
return arr[randint(arr.length)];
}
export function range(n: number): number[] {
return Array.from({length: n}, (x, i) => i);
}

View file

@ -1,190 +1,397 @@
<template> <template>
<div class="collage"> <div class="collage">
<div class="canvas"> <div class="canvas">
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas> <canvas
<div class="canvas-size"> id="canvas"
<label> ref="canvas"
Width: :width="canvasSize.width"
<input type="number" step="16" min="128" v-model="canvasSize.width"> :height="canvasSize.height"
</label> ></canvas>
<label> <div class="canvas-size">
Height: <label>
<input type="number" step="16" min="128" v-model="canvasSize.height"> Width:
</label> <input type="number" step="16" min="128" v-model="canvasSize.width" />
</div> </label>
<label>
Height:
<input
type="number"
step="16"
min="128"
v-model="canvasSize.height"
/>
</label>
</div>
</div> </div>
<div class="controls"> <div class="controls">
<div class="modes"> <div class="modes">
<label v-for="(mode, idx) in modes" <ul class="modes-list">
:class="{disabled: images.length < mode.minImages, <li
selected: idx === currentModeType, v-for="(mode, idx) in modes.modes"
lastActive: idx === lastActiveModeType}"> :class="[
{{mode.name}} 'mode',
<input type="radio" :value="idx" v-model="currentModeType"> {
</label> disabled: images.length < mode.minImages,
</div> excluded: excludedModes.includes(idx),
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button> selected: idx === currentModeType,
<hr> lastActive: lastActiveModeTypes.includes(idx),
<div class="config"> },
<label class="config-numimages"> ]"
#N of images: :key="idx"
<input type="number" :min="currentMode.minImages" :max="images.length" >
placeholder="RND" <label class="handle"
:disabled="Object.keys(forceConfig).includes('numImages')" >{{ mode.name
v-model="forceConfig.numImages || collageConfig.numImages"> }}<input type="radio" :value="idx" v-model="currentModeType"
</label> /></label>
</div> <span
class="mode-plus"
v-if="
['recursive', 'shuffle'].includes(currentModeType) &&
images.length >= minImages
"
>
<span class="separator"></span>
<label
>{{ excludedModes.includes(idx) ? "o" : "x"
}}<input type="checkbox" :value="idx" v-model="excludedModes"
/></label>
</span>
</li>
</ul>
<hr />
<label
:class="{
disabled: images.length < minImages,
selected: 'shuffle' === currentModeType,
}"
>
!SHUFFLE ALL!
<input type="radio" value="shuffle" v-model="currentModeType" />
</label>
<label
:class="{
disabled: images.length < minImages,
selected: 'recursive' === currentModeType,
}"
>
#RECURSIVE#
<input type="radio" value="recursive" v-model="currentModeType" />
</label>
</div>
<button :disabled="images.length < minImages" @click="renderCollage">
REPAINT
</button>
<hr />
<div class="config">
<template v-if="currentModeType !== 'recursive'">
<label class="config-numimages">
#N of images:
<input
type="number"
:min="minImages"
:max="images.length"
placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')"
:value="forceConfig.numImages || collageConfig.numImages"
@input="(n) => (collageConfig.numImages = n)"
/>
</label>
</template>
<template v-else>
<label>
Recursion levels:
<input
type="number"
:min="1"
:max="10"
v-model="recursiveConfig.level"
/>
</label>
<label>
<input type="checkbox" v-model="recursiveConfig.repeat" />
Repeat images?
</label>
</template>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import collageModes, {CollageModeType} from "../collages"; import BrowserCollageModes from "../collages";
import {CollageConfig, CollageMode} from "@/types"; import { CollageModeType } from "../common/collages";
import { CollageConfig, CollageMode, Segment } from "../common/types";
import { choice, shuffle } from "../common/utils";
type DisplayCollageModeType = CollageModeType | & "shuffle"; type DisplayCollageModeType = CollageModeType | "shuffle" | "recursive";
@Component @Component
export default class Collage extends Vue { export default class Collage extends Vue {
@Prop({required: true}) private images!: ImageBitmap[]; @Prop({ required: true }) private images!: ImageBitmap[];
private context!: CanvasRenderingContext2D; private context!: CanvasRenderingContext2D;
private canvasSize = { private canvasSize = {
width: 640, width: 640,
height: 640 height: 640,
}; };
private collageConfig: CollageConfig = { private collageConfig: CollageConfig = {
numImages: undefined numImages: undefined,
}; };
private currentModeType: DisplayCollageModeType = "shuffle"; private recursiveConfig = {
private lastActiveModeType: DisplayCollageModeType | null = null; level: 2,
private modes: { [key in DisplayCollageModeType]: CollageMode } = { repeat: true,
...collageModes, };
"shuffle": { private currentModeType: DisplayCollageModeType = "shuffle";
name: "Shuffle all!", private lastActiveModeTypes: CollageModeType[] = [];
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)), private excludedModes: CollageModeType[] = [];
place: (ctx, images, config) => { private modes = new BrowserCollageModes();
const permissibleModeKeys = Object.keys(collageModes)
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[]; private get minImages() {
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)]; if (
const randomMode = collageModes[randomModeType]; this.currentModeType === "shuffle" ||
this.setLastActiveModeType(randomModeType); this.currentModeType === "recursive"
randomMode.place(ctx, images, config); ) {
return Math.min(
...Object.values(this.modes.modes).map((mode) => mode.minImages)
);
} else {
return this.modes.modes[this.currentModeType].minImages;
}
}
private get lastMode() {
if (this.lastActiveModeTypes.length === 1) {
return this.modes.modes[this.lastActiveModeTypes[0]];
}
}
private get forceConfig() {
return this.lastMode ? this.lastMode.forceConfig || {} : {};
}
private mounted() {
const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
}
@Watch("images")
@Watch("currentModeType")
@Watch("collageConfig", { deep: true })
@Watch("recursiveConfig", { deep: true })
private renderCollage() {
if (this.images.length >= this.minImages) {
this.reset();
const permissibleModeKeys = (
Object.keys(this.modes.modes) as CollageModeType[]
).filter(
(k) =>
!this.excludedModes.includes(k) &&
this.modes.modes[k].minImages <= this.images.length
);
if (this.currentModeType !== "recursive") {
let mode: CollageMode<any, any>;
if (this.currentModeType === "shuffle") {
const randomModeType = choice(permissibleModeKeys);
this.lastActiveModeTypes = [randomModeType];
mode = this.modes.modes[randomModeType];
} else {
this.lastActiveModeTypes = [this.currentModeType];
mode = this.modes.modes[this.currentModeType];
}
const shuffledImages = shuffle(this.images);
const segments = mode.getSegments(
this.context,
this.collageConfig,
shuffledImages
);
mode.place(this.context, shuffledImages, segments);
} else {
this.lastActiveModeTypes = [];
const shuffledImages = shuffle(this.images);
const rootSegment: Segment = {
x: 0,
y: 0,
w: this.context.canvas.width,
h: this.context.canvas.height,
};
const processSegment = async (
segment: Segment,
level: number
): Promise<ImageBitmap> => {
console.debug(segment, level);
if (
segment === rootSegment ||
level <= this.recursiveConfig.level - 1
) {
let canvas = document.createElement("canvas");
canvas.width = segment.w;
canvas.height = segment.h;
let modeKey = choice(permissibleModeKeys);
console.debug(modeKey);
this.lastActiveModeTypes.push(modeKey);
let mode = this.modes.modes[modeKey];
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
let segments = mode.getSegments(ctx);
console.debug(segments);
let bitmaps = await Promise.all(
segments.map((segment) => processSegment(segment, level + 1))
);
mode.place(ctx, bitmaps, segments);
return await createImageBitmap(canvas);
} else {
if (this.recursiveConfig.repeat) {
return choice(shuffledImages);
} else {
if (shuffledImages.length > 0) {
return shuffledImages.pop() as ImageBitmap;
} else {
throw "RAN OUT OF IMAGES";
}
} }
} }
}; };
processSegment(rootSegment, 0)
// wtf vue? .then((finalCollage) => {
private setLastActiveModeType(lastActiveModeType: any) { console.debug(finalCollage);
this.lastActiveModeType = lastActiveModeType; this.context.drawImage(finalCollage, 0, 0);
})
.catch((error) => {
alert(error);
});
}
} }
}
private get currentMode() { @Watch("canvasSize", { deep: true })
return this.modes[this.currentModeType]; private onCanvasSizeChange() {
} this.$nextTick(() => {
this.renderCollage();
});
}
private get lastMode() { private reset() {
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined; this.context.globalCompositeOperation = "source-over";
} const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context.clearRect(0, 0, canvas.width, canvas.height);
private get forceConfig() { }
return this.lastMode ? this.lastMode.forceConfig || {} : {};
}
private mounted() {
const canvas = (this.$refs.canvas as HTMLCanvasElement);
this.context = canvas.getContext("2d") as CanvasRenderingContext2D;
}
@Watch("images")
@Watch("currentMode")
@Watch("collageConfig", {deep: true})
private renderCollage() {
if (this.images.length >= this.currentMode.minImages) {
this.lastActiveModeType = this.currentModeType;
this.currentMode.place(this.context, this.images, this.collageConfig);
}
}
@Watch("canvasSize", {deep: true})
private onCanvasSizeChange() {
this.$nextTick(() => {
this.renderCollage();
});
}
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<!--suppress CssUnusedSymbol -->
<style scoped> <style scoped>
.collage { .collage {
margin: 2rem; margin: 2rem;
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
align-items: center; align-items: center;
} }
.canvas { .canvas {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
#canvas { #canvas {
border: 1px solid black; border: 1px solid black;
} }
.canvas-size { .canvas-size {
margin: 1rem; margin: 1rem;
} }
.controls button { .controls button {
width: 100%; width: 100%;
} }
.modes { .modes {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
}
.modes-list {
list-style: none;
padding: 0;
}
.mode {
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.separator {
border: 1px solid gray;
width: 0;
margin: 0 0.5em;
align-self: stretch;
}
.mode .mode-plus,
.mode .mode-plus label {
color: gray;
font-size: 14px;
}
.mode-plus {
display: flex;
align-items: center;
}
.controls .modes hr {
margin-top: 0.5rem;
width: 100%;
color: lightgray;
} }
.controls label { .controls label {
font-size: 14pt; font-size: 14pt;
cursor: pointer; cursor: pointer;
margin: .25rem; margin: 0.25rem;
} }
.modes input { .modes input {
display: none; display: none;
} }
.controls button, .controls hr, .controls .config { .controls button,
margin-top: 1rem; .controls hr,
.controls .config {
margin-top: 1rem;
} }
.controls .config { .controls .config {
user-select: none; user-select: none;
} }
.config label { .config label {
display: block; display: block;
} }
.config-numimages input { .config-numimages input {
width: 4em; width: 4em;
} }
.disabled { .disabled {
color: gray; color: gray;
pointer-events: none; pointer-events: none;
}
.excluded .handle {
color: gray;
text-decoration: line-through;
} }
.selected { .selected {
font-weight: bold; font-weight: bold;
} }
.lastActive { .lastActive .handle {
text-decoration: underline; text-decoration: underline;
} }
</style> </style>

10
src/types.d.ts vendored
View file

@ -1,10 +0,0 @@
export interface CollageMode {
name: string;
minImages: number;
place: (ctx: CanvasRenderingContext2D, images: ImageBitmap[], config: CollageConfig) => void;
forceConfig?: CollageConfig;
}
export interface CollageConfig {
numImages?: number;
}

22
tgbot/Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM python:3-slim
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN pip install poetry
WORKDIR /app
COPY tgbot/poetry.lock tgbot/pyproject.toml /app/
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
COPY /tgbot/ /app/
COPY kollagen /usr/bin/kollagen
CMD python kollagen-bot/main.py

0
tgbot/README.rst Normal file
View file

BIN
tgbot/kollagen Executable file

Binary file not shown.

View file

@ -0,0 +1 @@
__version__ = '0.1.0'

270
tgbot/kollagen-bot/main.py Normal file
View file

@ -0,0 +1,270 @@
import logging
import os
import subprocess
from typing import List, Optional
from tempfile import NamedTemporaryFile
from telegram import Update
from telegram.ext import (
Updater,
CommandHandler,
DictPersistence,
CallbackContext,
)
from telegram.parsemode import ParseMode
from parser import SafeArgumentParser, safe_str
class KollagenBot:
def __init__(
self, tg_token: str, kollagen_path: str, base_dir: Optional[str]
) -> None:
self.logger = logging.getLogger("kollagen")
self.kollagen_path = kollagen_path
self.base_dir = os.path.abspath(base_dir) if base_dir else None
self._init_parser()
self.updater = Updater(
tg_token, persistence=DictPersistence(user_data_json="{}")
)
dispatcher = self.updater.dispatcher
dispatcher.add_handler(CommandHandler("start", self.tg_start))
dispatcher.add_handler(CommandHandler("help", self.tg_help))
dispatcher.add_handler(CommandHandler("generate", self.tg_generate))
dispatcher.add_handler(CommandHandler("g", self.tg_generate))
dispatcher.add_handler(CommandHandler("regenerate", self.tg_regenerate))
dispatcher.add_handler(CommandHandler("r", self.tg_regenerate))
dispatcher.add_error_handler(self.tg_error)
def _init_parser(self):
parser = SafeArgumentParser(prog="/generate", add_help=False)
parser.add_argument(
"directories",
metavar="path",
type=safe_str,
nargs="*",
default=[self.base_dir] if self.base_dir else [],
help="Directories or files to process. By default, the entire base directory is processed.",
)
parser.add_argument(
"-m",
dest="mode",
metavar="mode",
type=safe_str,
nargs="?",
const=True,
help=f"Collage modes to use. By default, one is chosen at random. Multiple modes can be specified, separated by commas. When no value is specified, all modes are listed.",
)
parser.add_argument(
"-n",
dest="num_images",
metavar="N",
type=int,
help=f"How many images to use in a single collage. Random (or collage-dependant) by default.",
)
parser.add_argument(
"-w",
dest="width",
type=int,
default=640,
help=f"Width of resulting output (in px). 640px by default.",
)
parser.add_argument(
"-h",
dest="height",
type=int,
default=640,
help=f"Height of resulting output (in px). 640px by default.",
)
parser.add_argument(
"--rm",
dest="recursive_modes",
type=safe_str,
help=f"Collage modes (comma-separated) to use in a recursive collage. All by default.",
)
parser.add_argument(
"--rl",
dest="recursive_level",
type=int,
default=2,
help=f"Level/depth of recursive collage. 2 by default.",
)
parser.add_argument(
"--rr",
dest="recursive_repeat",
action="store_true",
help=f"Allow repeating images in (different levels of) recursive collages. False by default.",
)
self.parser = parser
def _get_modes(self):
modes = subprocess.run(
[self.kollagen_path, "-m"], check=True, capture_output=True
)
return modes.stdout.decode("utf-8").strip().split(", ")
def tg_start(self, update: Update, context: CallbackContext):
update.message.reply_text("Hi! I make random collages. Check out https://gitlab.com/tmladek/kollagen and /help. Here's one to get you started:")
self._process([], update)
def tg_generate(self, update: Update, context: CallbackContext):
cmd_line = update.message.text.split(" ")[1:]
success = self._process(cmd_line, update)
if success and context.user_data is not None:
context.user_data["last_cmd_line"] = cmd_line
def tg_regenerate(self, update: Update, context: CallbackContext):
if context.user_data and context.user_data.get("last_cmd_line"):
self._process(context.user_data["last_cmd_line"], update)
else:
update.message.reply_text("No previous command to regenerate!")
def _process(self, cmd_line: List[str], update: Update):
self.logger.info(
f"Generating from {update.effective_user}, with cmd_line: `{cmd_line}`"
)
args = self.parser.parse_args(cmd_line)
if args.mode is True:
update.message.reply_text(
f"Available modes: {', '.join(self._get_modes())}"
)
return False
directories = []
for dir in args.directories:
for possible in [
os.path.join(self.base_dir or "./", dir),
os.path.join(self.base_dir or "./", dir.upper()),
]:
if os.path.exists(possible):
directories.append(possible)
break
else:
raise FileNotFoundError(f'"{dir}" does not exist.')
mode = ["-m", args.mode] if args.mode else []
num_images = ["-n", str(args.num_images)] if args.num_images else []
recursive_level = (
["--rl", str(args.recursive_level)] if args.recursive_level else []
)
recursive_repeat = ["--rr"] if args.recursive_repeat else []
recursive_modes = (
["--rm", str(args.recursive_modes)] if args.recursive_modes else []
)
with NamedTemporaryFile(suffix=".png") as ntf:
shell_cmd_line = [
self.kollagen_path,
*directories,
"-w",
str(args.width),
"-h",
str(args.height),
*mode,
*num_images,
*recursive_level,
*recursive_repeat,
*recursive_modes,
"-o",
ntf.name,
]
self.logger.debug(f"Running: " + str(shell_cmd_line))
result = subprocess.run(
shell_cmd_line,
check=True,
capture_output=True,
timeout=60,
env={
'NO_COLOR': "1"
}
)
ntf.seek(0)
used_line = next(
(
line
for line in result.stdout.decode("utf-8").splitlines()
if line.startswith("Used: ")
),
"",
).replace(f"{self.base_dir}/" if self.base_dir else "", "")
caption = ""
caption += (
f"`{' '.join(['/generate', *cmd_line])}`\n" if len(cmd_line) else ""
)
caption += used_line.replace("_", "\\_")
caption = caption[:200]
update.message.reply_photo(
ntf,
caption=caption,
parse_mode=ParseMode.MARKDOWN,
)
return True
def tg_help(self, update: Update, context: CallbackContext):
update.message.reply_text(
f"```{self.parser.format_help()}```", parse_mode=ParseMode.MARKDOWN
)
def tg_error(self, update: object, context: CallbackContext) -> None:
self.logger.error(
msg="Exception while handling an update:", exc_info=context.error
)
if isinstance(update, Update):
if isinstance(context.error, subprocess.CalledProcessError):
error_display = context.error.stderr.decode('utf-8')
else:
error_display = str(context.error)
error_display = error_display[:2500]
update.message.reply_text(
f"Something is fucked!\n{error_display}"
)
def start_idle(self):
self.updater.start_polling()
self.updater.idle()
def main() -> None:
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=getattr(logging, os.getenv("LOG_LEVEL", "info").upper()),
)
tg_token = os.getenv("TG_TOKEN")
if not tg_token:
logging.error("TG_TOKEN is required.")
exit(1)
if env_kollagen_path := os.getenv("KOLLAGEN_PATH"):
if os.path.exists(env_kollagen_path):
kollagen_path = env_kollagen_path
else:
logging.error(f"kollagen not found! {env_kollagen_path} does not exist.")
exit(1)
else:
which = subprocess.run(["which", "kollagen"], capture_output=True)
try:
which.check_returncode()
kollagen_path = which.stdout.decode("utf-8").strip()
except subprocess.CalledProcessError:
logging.error(
"kollagen not found! KOLLAGEN_PATH not specified and `kollagen` isn't in $PATH."
)
exit(1)
bot = KollagenBot(tg_token, kollagen_path, os.getenv("BASE_DIR"))
bot.start_idle()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,16 @@
import argparse
import re
class ArgumentParserError(RuntimeError):
pass
class SafeArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise ArgumentParserError(message)
def safe_str(val: str):
if re.findall(r'[^\w,]', val):
raise RuntimeError("No special characters in arguments allowed!")
return val

393
tgbot/poetry.lock generated Normal file
View file

@ -0,0 +1,393 @@
[[package]]
name = "apscheduler"
version = "3.6.3"
description = "In-process task scheduler with Cron-like capabilities"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pytz = "*"
six = ">=1.4.0"
tzlocal = ">=1.2"
[package.extras]
asyncio = ["trollius"]
doc = ["sphinx", "sphinx-rtd-theme"]
gevent = ["gevent"]
mongodb = ["pymongo (>=2.8)"]
redis = ["redis (>=3.0)"]
rethinkdb = ["rethinkdb (>=2.4.0)"]
sqlalchemy = ["sqlalchemy (>=0.8)"]
testing = ["pytest", "pytest-cov", "pytest-tornado5", "mock", "pytest-asyncio (<0.6)", "pytest-asyncio"]
tornado = ["tornado (>=4.3)"]
twisted = ["twisted"]
zookeeper = ["kazoo"]
[[package]]
name = "backports.zoneinfo"
version = "0.2.1"
description = "Backport of the standard library zoneinfo module"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
tzdata = ["tzdata"]
[[package]]
name = "black"
version = "21.9b0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
[package.dependencies]
click = ">=7.1.2"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0,<1"
platformdirs = ">=2"
regex = ">=2020.1.8"
tomli = ">=0.2.6,<2.0.0"
typing-extensions = [
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
]
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
python2 = ["typed-ast (>=1.4.2)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cachetools"
version = "4.2.2"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.5"
[[package]]
name = "certifi"
version = "2021.5.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "click"
version = "8.0.1"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "platformdirs"
version = "2.3.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "python-telegram-bot"
version = "13.7"
description = "We have made you a wrapper you can't refuse"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
APScheduler = "3.6.3"
cachetools = "4.2.2"
certifi = "*"
pytz = ">=2018.6"
tornado = ">=6.1"
[package.extras]
json = ["ujson"]
passport = ["cryptography (!=3.4,!=3.4.1,!=3.4.2,!=3.4.3)"]
socks = ["pysocks"]
[[package]]
name = "pytz"
version = "2021.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "regex"
version = "2021.8.28"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "1.2.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "tornado"
version = "6.1"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
category = "main"
optional = false
python-versions = ">= 3.5"
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "tzdata"
version = "2021.1"
description = "Provider of IANA time zone data"
category = "main"
optional = false
python-versions = ">=2"
[[package]]
name = "tzlocal"
version = "3.0"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
test = ["pytest-mock (>=3.3)", "pytest (>=4.3)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "a1390250d2dc7c6ca705c53cebcec951a6dd79717bb0f902b0177b307fac8bc3"
[metadata.files]
apscheduler = [
{file = "APScheduler-3.6.3-py2.py3-none-any.whl", hash = "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526"},
{file = "APScheduler-3.6.3.tar.gz", hash = "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244"},
]
"backports.zoneinfo" = [
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
{file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
{file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
{file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
{file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
]
black = [
{file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
{file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
]
cachetools = [
{file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"},
{file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
{file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
]
python-telegram-bot = [
{file = "python-telegram-bot-13.7.tar.gz", hash = "sha256:24df75459e335b96baffa6991679f844bd426978af5a69ca419a0ac43a40602c"},
{file = "python_telegram_bot-13.7-py3-none-any.whl", hash = "sha256:3bf210862744068aa789d5110f8e3a00d98912ce50863384836440a18abf76b5"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
regex = [
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
tomli = [
{file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
{file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
]
tornado = [
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
{file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
{file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
{file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
{file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
{file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
{file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
{file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
{file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
{file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
{file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
{file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
{file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
{file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
{file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
{file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
{file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
{file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
{file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
{file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
{file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
{file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
{file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
{file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
{file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
{file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
{file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
{file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
{file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
{file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
{file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
tzdata = [
{file = "tzdata-2021.1-py2.py3-none-any.whl", hash = "sha256:9ad21eada54c97001e3e9858a674b3ee6bebe4a4fb2b58465930f2af0ba6c85d"},
{file = "tzdata-2021.1.tar.gz", hash = "sha256:e19c7351f887522a1ac739d21041e592ddde6dd1b764fdefa8f7b2b3551d3d38"},
]
tzlocal = [
{file = "tzlocal-3.0-py3-none-any.whl", hash = "sha256:c736f2540713deb5938d789ca7c3fc25391e9a20803f05b60ec64987cf086559"},
{file = "tzlocal-3.0.tar.gz", hash = "sha256:f4e6e36db50499e0d92f79b67361041f048e2609d166e93456b50746dc4aef12"},
]

16
tgbot/pyproject.toml Normal file
View file

@ -0,0 +1,16 @@
[tool.poetry]
name = "kollagen-bot"
version = "0.1.0"
description = ""
authors = ["Tomáš Mládek <t@mldk.cz>"]
[tool.poetry.dependencies]
python = "^3.8"
python-telegram-bot = "^13.7"
[tool.poetry.dev-dependencies]
black = {version = "^21.9b0", allow-prereleases = true}
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"