Compare commits

...

57 Commits

Author SHA1 Message Date
Tomáš Mládek c89080ff00
autoformat 2022-01-19 22:00:29 +01:00
Tomáš Mládek f24047b33b
fix canvas version mismatch breaking build 2022-01-19 20:52:02 +01:00
Tomáš Mládek f004daaa3c add docker deno build to Makefile, clean step, fix app build, .gitignore 2021-09-23 12:38:41 +02:00
Tomáš Mládek ff39c8eeb3 bot error reporting does not use colors & truncates at 2500 chars 2021-09-22 23:57:42 +02:00
Tomáš Mládek cf19301f96 add 60s timeout to bot commands to prevent lockups 2021-09-22 23:57:06 +02:00
Tomáš Mládek a5068e4b64 fix bot --rr flag; tidy up bot /help & /start; cosmetics 2021-09-22 23:56:42 +02:00
Tomáš Mládek 04ba5b7e4e show used images in CLI and bot; caption cmdline in bot 2021-09-22 23:55:48 +02:00
Tomáš Mládek 546102205e fix recursive defaults 2021-09-20 23:06:11 +02:00
Tomáš Mládek fd06a2a618 add recursive args to bot as well 2021-09-20 23:05:24 +02:00
Tomáš Mládek e391b9b6bf add cli params for recursive modes 2021-09-20 22:56:02 +02:00
Tomáš Mládek bfe739a487 recursive traversal is implicit 2021-09-20 22:43:58 +02:00
Tomáš Mládek 85881f877c cli - reformat & lint 2021-09-20 22:41:00 +02:00
Tomáš Mládek 4d8b0b85dd cli - use proper defaults 2021-09-20 22:40:12 +02:00
Tomáš Mládek 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
Tomáš Mládek 2647238410 check both cases for directories 2021-09-19 13:47:52 +02:00
Tomáš Mládek 405fd3cad5 checks for nonexistent dirs 2021-09-19 13:44:35 +02:00
Tomáš Mládek 9a13c3c393 ...and -n 2021-09-19 13:36:31 +02:00
Tomáš Mládek 1e1faaa4c9 fix width/height args 2021-09-19 13:34:29 +02:00
Tomáš Mládek 0edc9a7ab4 fix args 2021-09-19 13:32:50 +02:00
Tomáš Mládek 8915a36b3d actual cmdline parsing in tg 2021-09-19 13:19:00 +02:00
Tomáš Mládek d443fc6d7e fix persistence (again) 2021-09-19 12:21:20 +02:00
Tomáš Mládek 6091c19c8b update gitignore 2021-09-19 12:19:31 +02:00
Tomáš Mládek b74d5838eb fix persistence 2021-09-19 12:18:32 +02:00
Tomáš Mládek 918e021eed fix lint 2021-09-19 12:14:41 +02:00
Tomáš Mládek 8ea2782038 remove clamping down files, improve logging 2021-09-19 12:09:17 +02:00
Tomáš Mládek 7e41fe054e autoformat & reasonable /start command 2021-09-19 12:09:15 +02:00
Tomáš Mládek 7e6a761363 lazy load images in deno 2021-09-19 12:08:46 +02:00
Tomáš Mládek 7919e4fb7d makefile improvements 2021-09-19 11:30:28 +02:00
Tomáš Mládek e82fcd5d42 error logging 2021-09-19 11:30:17 +02:00
Tomáš Mládek c89777621a unstable in .gitlab-ci.yml 2021-09-19 10:34:09 +02:00
Tomáš Mládek 3409978203 rudimentary tg bot 2021-09-19 10:31:34 +02:00
Tomáš Mládek d8dff75411 deno build --unstable 2021-09-19 09:48:25 +02:00
Tomáš Mládek 944dc9ca5f add a --recursive option to deno cli 2021-09-18 19:04:29 +02:00
Tomáš Mládek 26ceccbde2 formatting 2021-09-18 18:58:16 +02:00
Tomáš Mládek e34c3a96c4 pin specific canvas version in deno 2021-09-18 18:57:38 +02:00
Tomáš Mládek 86c9644972 -m param without args list modes 2021-09-18 12:11:03 +02:00
Tomáš Mládek e9bc9f568d limit cli only to jpgs/pngs (by default) 2021-09-18 11:42:44 +02:00
Tomáš Mládek eb455e2311 clamp cli to 25 images for performance reasons 2021-09-17 19:09:51 +02:00
Tomáš Mládek 57f32ef643 cli takes directories as well as files 2021-09-17 18:56:57 +02:00
Tomáš Mládek cd76a36d99 adjust .gitlab-ci.yml to also build app, remove Makefile dep 2021-09-15 21:29:48 +02:00
Tomáš Mládek 71af6bb680 add base CI for Deno 2021-09-15 21:22:18 +02:00
Tomáš Mládek 6720eace7a add Makefile 2021-09-15 21:19:22 +02:00
Tomáš Mládek 0982857082 fix linter errors, reformat 2021-09-15 21:19:03 +02:00
Tomáš Mládek 1ba396bc7c make image loading work even in compiled version (?) 2021-09-15 21:16:01 +02:00
Tomáš Mládek a656d37d8c reformat cli/main.ts 2021-09-15 21:15:41 +02:00
Tomáš Mládek 14efcf03a1 unnecessary files, fix default extension 2021-09-15 20:56:40 +02:00
Tomáš Mládek b5a6b70c2b npx browserslist@latest --update-db 2021-09-15 19:14:58 +02:00
Tomáš Mládek 5734774366 autoformat, fix lint errors 2021-09-15 19:13:26 +02:00
Tomáš Mládek 054f9a9218 deno cli version (ugly) proof of concept 2021-09-15 19:09:44 +02:00
Tomáš Mládek 79f9108f34 autoformat 2021-09-11 23:51:22 +02:00
Tomáš Mládek de53e1b741 add CHANGELOG.md 2020-07-20 15:11:22 +02:00
Tomáš Mládek d319de86d9 version bump 2020-07-20 14:57:14 +02:00
Tomáš Mládek f54b61e236 add exclusion for shuffle & recursive modes 2020-07-20 14:57:05 +02:00
Tomáš Mládek c481d5739f version bump proper 2020-07-17 19:57:46 +02:00
Tomáš Mládek 89091dcd24 version bump 2020-07-17 19:14:52 +02:00
Tomáš Mládek 64fa7098a0 add recursion 2020-07-17 19:14:42 +02:00
Tomáš Mládek 7cf393af71 major refactor (add segments) in preparation for recursive collages 2020-07-17 16:11:40 +02:00
24 changed files with 1846 additions and 337 deletions

4
.gitignore vendored
View File

@ -1,5 +1,6 @@
.DS_Store
node_modules
.npm
/dist
# local env files
@ -20,3 +21,6 @@ pnpm-debug.log*
*.njsproj
*.sln
*.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
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",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -2810,9 +2810,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001085",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001085.tgz",
"integrity": "sha512-x0YRFRE0pmOD90z+9Xk7jwO58p4feVNXP+U8kWV+Uo/HADyrgESlepzIkUqPgaXkpyceZU6siM1gsK7sHgplqA==",
"version": "1.0.30001257",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
"integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==",
"dev": true
},
"case-sensitive-paths-webpack-plugin": {

View File

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

View File

@ -1,193 +1,14 @@
import {CollageMode} from "@/types";
import {choice, randint, shuffle} from "@/utils";
import { CollageModes } from "./common/collages";
const collageModeType = [
"clean_grid", "chaos_grid",
"row", "irow",
"col", "icol",
"concentric_factor", "concentric_spaced",
"blend"
] as const;
export type CollageModeType = typeof collageModeType[number];
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;
export default class BrowserCollageModes extends CollageModes<CanvasRenderingContext2D, ImageBitmap, any> {
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);
}
return [x, y];
}
const modes: { [key in CollageModeType]: CollageMode } = {
"clean_grid": {
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) {
factor = choice([1 / Math.sqrt(2), .5, .88]);
} 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);
selectedImages.forEach((image, idx) => {
cleanDraw(
ctx, image,
ctx.canvas.width / 2,
ctx.canvas.height / 2,
ctx.canvas.width - (ctx.canvas.width / selectedImages.length * idx),
ctx.canvas.height - (ctx.canvas.height / selectedImages.length * idx),
);
});
}
},
"blend": {
name: "Blending",
minImages: 2,
place: (ctx, images, config) => {
const selectedImages = shuffle(images).slice(0, config.numImages || randint(2) + 2);
ctx.globalCompositeOperation = choice(["difference", "saturation", "soft-light", "overlay"]);
selectedImages.forEach((image) => {
cleanDraw(
ctx, image,
ctx.canvas.width / 2, ctx.canvas.height / 2,
ctx.canvas.width, ctx.canvas.height
);
});
}
createCanvas(w: number, h: number) {
throw new Error("Method not implemented.");
}
};
export default modes;
canvasToImage(canvas: any): PromiseLike<ImageBitmap> {
throw new Error("Method not implemented.");
}
}

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

@ -18,6 +18,10 @@ export function randint(n: number) {
return Math.floor(Math.random() * n);
}
export function choice<T>(arr: T[]): T {
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,124 +1,281 @@
<template>
<div class="collage">
<div class="collage">
<div class="canvas">
<canvas id="canvas" ref="canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
<div class="canvas-size">
<label>
Width:
<input type="number" step="16" min="128" v-model="canvasSize.width">
</label>
<label>
Height:
<input type="number" step="16" min="128" v-model="canvasSize.height">
</label>
</div>
<canvas
id="canvas"
ref="canvas"
:width="canvasSize.width"
:height="canvasSize.height"
></canvas>
<div class="canvas-size">
<label>
Width:
<input type="number" step="16" min="128" v-model="canvasSize.width" />
</label>
<label>
Height:
<input
type="number"
step="16"
min="128"
v-model="canvasSize.height"
/>
</label>
</div>
</div>
<div class="controls">
<div class="modes">
<label v-for="(mode, idx) in modes"
:class="{disabled: images.length < mode.minImages,
selected: idx === currentModeType,
lastActive: idx === lastActiveModeType}">
{{mode.name}}
<input type="radio" :value="idx" v-model="currentModeType">
</label>
</div>
<button :disabled="images.length < currentMode.minImages" @click="renderCollage">REPAINT</button>
<hr>
<div class="config">
<label class="config-numimages">
#N of images:
<input type="number" :min="currentMode.minImages" :max="images.length"
placeholder="RND"
:disabled="Object.keys(forceConfig).includes('numImages')"
v-model="forceConfig.numImages || collageConfig.numImages">
</label>
</div>
<div class="modes">
<ul class="modes-list">
<li
v-for="(mode, idx) in modes.modes"
:class="[
'mode',
{
disabled: images.length < mode.minImages,
excluded: excludedModes.includes(idx),
selected: idx === currentModeType,
lastActive: lastActiveModeTypes.includes(idx),
},
]"
:key="idx"
>
<label class="handle"
>{{ mode.name
}}<input type="radio" :value="idx" v-model="currentModeType"
/></label>
<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>
</template>
<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
import collageModes, {CollageModeType} from "../collages";
import {CollageConfig, CollageMode} from "@/types";
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import BrowserCollageModes from "../collages";
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
export default class Collage extends Vue {
@Prop({required: true}) private images!: ImageBitmap[];
private context!: CanvasRenderingContext2D;
private canvasSize = {
width: 640,
height: 640
};
private collageConfig: CollageConfig = {
numImages: undefined
};
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeType: DisplayCollageModeType | null = null;
private modes: { [key in DisplayCollageModeType]: CollageMode } = {
...collageModes,
"shuffle": {
name: "Shuffle all!",
minImages: Math.min(...Object.values(collageModes).map(m => m.minImages)),
place: (ctx, images, config) => {
const permissibleModeKeys = Object.keys(collageModes)
.filter(k => collageModes[k as CollageModeType].minImages <= images.length) as CollageModeType[];
const randomModeType = permissibleModeKeys[Math.floor(Math.random() * permissibleModeKeys.length)];
const randomMode = collageModes[randomModeType];
this.setLastActiveModeType(randomModeType);
randomMode.place(ctx, images, config);
@Prop({ required: true }) private images!: ImageBitmap[];
private context!: CanvasRenderingContext2D;
private canvasSize = {
width: 640,
height: 640,
};
private collageConfig: CollageConfig = {
numImages: undefined,
};
private recursiveConfig = {
level: 2,
repeat: true,
};
private currentModeType: DisplayCollageModeType = "shuffle";
private lastActiveModeTypes: CollageModeType[] = [];
private excludedModes: CollageModeType[] = [];
private modes = new BrowserCollageModes();
private get minImages() {
if (
this.currentModeType === "shuffle" ||
this.currentModeType === "recursive"
) {
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";
}
}
}
};
// wtf vue?
private setLastActiveModeType(lastActiveModeType: any) {
this.lastActiveModeType = lastActiveModeType;
}
};
processSegment(rootSegment, 0)
.then((finalCollage) => {
console.debug(finalCollage);
this.context.drawImage(finalCollage, 0, 0);
})
.catch((error) => {
alert(error);
});
}
}
}
private get currentMode() {
return this.modes[this.currentModeType];
}
@Watch("canvasSize", { deep: true })
private onCanvasSizeChange() {
this.$nextTick(() => {
this.renderCollage();
});
}
private get lastMode() {
return this.lastActiveModeType ? this.modes[this.lastActiveModeType] : undefined;
}
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.reset();
this.currentMode.place(this.context, this.images, this.collageConfig);
}
}
@Watch("canvasSize", {deep: true})
private onCanvasSizeChange() {
this.$nextTick(() => {
this.renderCollage();
});
}
private reset() {
this.context.globalCompositeOperation = "source-over";
const canvas = (this.$refs.canvas as HTMLCanvasElement);
this.context.clearRect(0, 0, canvas.width, canvas.height);
}
private reset() {
this.context.globalCompositeOperation = "source-over";
const canvas = this.$refs.canvas as HTMLCanvasElement;
this.context.clearRect(0, 0, canvas.width, canvas.height);
}
}
</script>
@ -126,72 +283,115 @@ export default class Collage extends Vue {
<!--suppress CssUnusedSymbol -->
<style scoped>
.collage {
margin: 2rem;
display: flex;
justify-content: space-evenly;
align-items: center;
margin: 2rem;
display: flex;
justify-content: space-evenly;
align-items: center;
}
.canvas {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
}
#canvas {
border: 1px solid black;
border: 1px solid black;
}
.canvas-size {
margin: 1rem;
margin: 1rem;
}
.controls button {
width: 100%;
width: 100%;
}
.modes {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
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 {
font-size: 14pt;
cursor: pointer;
margin: .25rem;
font-size: 14pt;
cursor: pointer;
margin: 0.25rem;
}
.modes input {
display: none;
display: none;
}
.controls button, .controls hr, .controls .config {
margin-top: 1rem;
.controls button,
.controls hr,
.controls .config {
margin-top: 1rem;
}
.controls .config {
user-select: none;
user-select: none;
}
.config label {
display: block;
display: block;
}
.config-numimages input {
width: 4em;
width: 4em;
}
.disabled {
color: gray;
pointer-events: none;
color: gray;
pointer-events: none;
}
.excluded .handle {
color: gray;
text-decoration: line-through;
}
.selected {
font-weight: bold;
font-weight: bold;
}
.lastActive {
text-decoration: underline;
.lastActive .handle {
text-decoration: underline;
}
</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"