diff --git a/packages/import-hoj/index.ts b/packages/import-hoj/index.ts
new file mode 100644
index 00000000..0e8a725a
--- /dev/null
+++ b/packages/import-hoj/index.ts
@@ -0,0 +1,142 @@
+/* eslint-disable no-await-in-loop */
+import os from 'os';
+import path from 'path';
+import {
+ AdmZip, buildContent, Context, fs, Handler, PERM,
+ ProblemConfigFile, ProblemModel, ValidationError, yaml,
+} from 'hydrooj';
+
+const tmpdir = path.join(os.tmpdir(), 'hydro', 'import-hoj');
+fs.ensureDirSync(tmpdir);
+
+class ImportHojHandler extends Handler {
+ async fromFile(domainId: string, zipfile: string) {
+ let zip: AdmZip;
+ try {
+ zip = new AdmZip(zipfile);
+ } catch (e) {
+ throw new ValidationError('zip', null, e.message);
+ }
+ const tmp = path.resolve(tmpdir, String.random(32));
+ await new Promise((resolve, reject) => {
+ zip.extractAllToAsync(tmp, true, (err) => (err ? reject(err) : resolve(null)));
+ });
+ let cnt = 0;
+ try {
+ const folders = await fs.readdir(tmp, { withFileTypes: true });
+ for (const { name: folder } of folders.filter((i) => i.isDirectory())) {
+ if (!fs.existsSync(path.join(tmp, `${folder}.json`))) continue;
+ const buf = await fs.readFile(path.join(tmp, `${folder}.json`));
+ const doc = JSON.parse(buf.toString());
+ const pdoc = doc.problem;
+ const content = {
+ description: pdoc.description,
+ input: pdoc.input,
+ output: pdoc.output,
+ samples: [],
+ hint: pdoc.hint,
+ source: pdoc.source,
+ };
+ if (pdoc.examples) {
+ const re = /([\s\S]*?)<\/input>