ひととおり動いた

This commit is contained in:
Xeltica 2020-07-24 01:14:28 +09:00
commit b958350d0f
21 changed files with 3559 additions and 0 deletions

36
.eslintrc.js Normal file
View File

@ -0,0 +1,36 @@
module.exports = {
'env': {
'browser': true,
'es2020': true
},
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 11,
'sourceType': 'module'
},
'plugins': [
'@typescript-eslint'
],
'rules': {
'indent': [
'error',
'tab'
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
}
};

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
built
yarn-error.log

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
simpkey
Copyright (C) 2020 Xeltica
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# Simpkey
**Use misskey without JavaScript 🥴**
Simpkey is a HTML-Form based server-side-processing misskey client.
It is suitable if you are using a legacy computer, or you are not prefer to enable JavaScript.
## build
```
yarn install
yarn build
yarn start
```
## LICENSE
[AGPL 3.0](LICENSE)

5
nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "ts,pug",
"exec": "npm-run-all -p tsc copy -s start"
}

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "simpkey",
"version": "1.0.0",
"description": "server-side misskey client",
"main": "built/app.js",
"author": "Xeltica",
"private": true,
"scripts": {
"tsc": "tsc",
"start": "node built/app.js",
"lint": "eslint src/index.ts",
"lint:fix": "eslint --fix src/index.ts",
"copy": "copyfiles -u 1 src/views/*.pug ./built/",
"clean": "rimraf built",
"build": "run-p tsc copy",
"watch": "nodemon",
"pope": "echo ぽぺ",
"bebeyo": "echo ベベヨ",
"chasmo": "echo チャスモハァーワ」。"
},
"dependencies": {
"@types/koa-bodyparser": "^4.3.0",
"axios": "^0.19.2",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"koa-router": "^9.1.0",
"koa-session": "^6.0.0",
"koa-views": "^6.3.0",
"pug": "^3.0.0",
"typescript": "^3.9.7"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/koa": "^2.11.3",
"@types/koa-router": "^7.4.1",
"@types/koa-session": "^5.10.2",
"@types/koa-views": "^2.0.4",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"copyfiles": "^2.3.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"rimraf": "^3.0.2"
}
}

19
src/app.ts Normal file
View File

@ -0,0 +1,19 @@
import Koa from 'koa';
import { router, render } from '.';
import config from './config';
import session from 'koa-session';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
console.log('Simpkey v' + config.version);
app.use(bodyParser());
app.use(render);
app.use(router.routes());
app.use(router.allowedMethods());
console.log('App launched!');
app.listen(3000);

6
src/config.ts Normal file
View File

@ -0,0 +1,6 @@
export default {
version: '1.0.0',
changelog: [
'initial release'
],
};

100
src/index.ts Normal file
View File

@ -0,0 +1,100 @@
import { Context, DefaultState } from 'koa';
import views from 'koa-views';
import Router from 'koa-router';
import config from './config';
import { signIn, api, i } from './misskey';
import { Note } from './models/Note';
import { User } from './models/User';
export const die = (ctx: Context, error: string): Promise<void> => {
ctx.status = 400;
return ctx.render('error', { error });
};
export const render = views(__dirname + '/views', {
extension: 'pug', options: {
...config,
getAcct: (user: User) => user.host ? `@${user.username}@${user.host}` : `@${user.username}`,
getUserName: (user: User) => user.name || user.username,
}
});
export const router = new Router<DefaultState, Context>();
const staticRouting = [
[ 'about', 'Simpkey について' ],
[ 'terms', '利用規約' ],
[ 'privacy-policy', 'プライバシーポリシー' ],
];
for (const [ name, title ] of staticRouting) {
router.get('/' + name, async (ctx, next) => {
await ctx.render(name, { title });
await next();
});
}
async function timeline(ctx: Context, host: string, endpoint: string, timelineName: string, token: string) {
const user = await i(host, token);
const timeline = await api<Note[]>(host, endpoint, { i: token });
await ctx.render('timeline', {
title: timelineName + ' - Simpkey',
user, timeline, timelineName
});
}
router.get('/ltl', async (ctx, next) => {
const token = ctx.cookies.get('i');
const host = ctx.cookies.get('host');
if (!token || !host) {
await die(ctx, 'ログインしてください');
} else {
const meta = await api<any>(host, 'meta', { i: token });
if (meta.disableLocalTimeline) {
await die(ctx, 'ローカルタイムラインは無効化されています');
} else {
await timeline(ctx, host, 'notes/local-timeline', 'ローカルタイムライン', token);
}
}
await next();
});
router.get('/', async (ctx, next) => {
const token = ctx.cookies.get('i');
const host = ctx.cookies.get('host');
if (!token || !host) {
console.log('no session so show top page');
await ctx.render('index', {
title: 'Simpkey'
});
} else {
console.log('show timeline with the session');
await timeline(ctx, host, 'notes/timeline', 'ホームタイムライン', token);
}
await next();
});
router.post('/', async (ctx) => {
const {
host,
username,
password,
token
} = ctx.request.body;
if (!host || !username || !password) {
await die(ctx, 'パラメータが足りません');
return;
}
try {
const { id, i } = await signIn(host, username, password, token);
ctx.cookies.set('id', id);
ctx.cookies.set('host', host);
ctx.cookies.set('i', i);
console.log('login as ' + username);
ctx.redirect('/');
} catch (err) {
await die(ctx, err.message);
console.error(err);
}
});

28
src/misskey.ts Normal file
View File

@ -0,0 +1,28 @@
import axios from 'axios';
import { User } from './models/User';
import { Note } from './models/Note';
export async function api<T>(host: string, endpoint: string, opts: { [_: string]: string }): Promise<T> {
const res = await axios.post<T>(`https://${host}/api/${endpoint}`, opts);
return res.data;
}
type SignInResult = { id: string, i: string };
export function signIn(host: string, username: string, password: string, token?: string): Promise<SignInResult> {
return api<SignInResult>(host, 'signin', {
username, password, ...( token ? { token } : { } )
});
}
export function i(host: string, i: string): Promise<User> {
return api<User>(host, 'i', { i });
}
export function usersShow(host: string, userId: string): Promise<User> {
return api<User>(host, 'users/show', { userId });
}
export function notesShow(host: string, noteId: string): Promise<Note> {
return api<Note>(host, 'notes/show', { noteId });
}

27
src/models/Note.ts Normal file
View File

@ -0,0 +1,27 @@
import { User } from './User';
export type NoteVisibility = 'public' | 'home' | 'followers' | 'specified';
export interface Note {
createdAt: string;
cw: string | null;
// emojis: Emoji[];
fileIds: string[];
// files: DriveFile[];
id: string;
reactions: Record<string, number>;
renoteCount: 0;
renoteId: string | null;
renote?: Note;
repliesCount: 0;
replyId: string | null;
reply?: Note;
text: string | null;
uri: string | null;
url: string | null;
user: User,
userId: string;
visibility: NoteVisibility;
localOnly: boolean;
viaMobile: boolean;
}

43
src/models/User.ts Normal file
View File

@ -0,0 +1,43 @@
import { Note } from './Note';
export interface User {
alwaysMarkNsfw: boolean;
autoAcceptFollowed: boolean;
avatarUrl: string;
bannerUrl: string;
birthday: string;
carefulBot: boolean;
createdAt: string;
description: string | null;
fields: { name: string, value: string }[];
followersCount: number;
followingCount: number;
hasPendingReceivedFollowRequest: boolean;
hasUnreadAnnouncement: boolean;
hasUnreadAntenna: boolean;
hasUnreadMentions: boolean;
hasUnreadMessagingMessage: boolean;
hasUnreadNotification: boolean;
hasUnreadSpecifiedNotes: boolean;
host: string | null;
id: string;
injectFeaturedNote: boolean;
isAdmin: boolean;
isBot: boolean;
isCat: boolean;
isLocked: boolean;
isModerator: boolean;
isSilenced: boolean;
isSuspended: boolean;
location: string | null;
name: string | null;
notesCount: number;
pinnedNotes: Note[];
// pinnedPage: Page;
token?: string;
twoFactorEnabled: boolean;
updatedAt: string;
url: string | null;
usePasswordLessLogin: boolean;
username: string;
}

35
src/views/_base.pug Normal file
View File

@ -0,0 +1,35 @@
mixin avatar(user)
img.avatar(src=user.avatarUrl, alt="avatar for " + user.username style="width: 64px; height: 64px; border-radius: 50%")
mixin note(note)
.note(id=note.id)
+avatar(note.user)
p: b=getUserName(note.user)
| &nbsp;
span(style="color: gray")= getAcct(note.user)
p= note.text
aside= new Date(note.createdAt).toLocaleString()
aside= note.visibility
html
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
block meta
title= title
body
header
h1: a(href="/") Simpkey
block header
main
block content
footer
hr
div
a(href="/privacy-policy") プライバシーポリシー
| ・
a(href="/terms") 利用規約
p (C)2020 Xeltica -
a(href="/about") version !{version}
block footer

8
src/views/about.pug Normal file
View File

@ -0,0 +1,8 @@
extends _base
block content
p Simpkey は、JavaScript のいらない Misskey クライアントです。
h2 バージョン !{version}
ul
each val in changelog
li= val

6
src/views/error.pug Normal file
View File

@ -0,0 +1,6 @@
extends _base
block content
h2 エラー
p= error || '不明なエラーです'
p: a(href="/") トップページに戻る

15
src/views/index.pug Normal file
View File

@ -0,0 +1,15 @@
extends _base
block content
p Simpkey へようこそ
p 使用するインスタンス、ユーザー名、パスワードを入力して、今すぐ始めましょう。
form(action="/", method="post")
div: label インスタンス名:
input(type="text", name="host", placeholder="例: misskey.io")
div: label ユーザー名:
input(type="text", name="username")
div: label パスワード:
input(type="password", name="password")
div: label 2段階認証コード (必要なら):
input(type="text", name="token")
button(type="submit") ログイン

View File

@ -0,0 +1,20 @@
extends _base
block content
h2 プライバシーポリシー
p 本サイトのプライバシーポリシーを以下に示します。本サイトのサービスをご利用いただいた時点で、自動的にポリシーに同意したものとみなされます。
h3 個人情報の利用目的
p 本サイトでは、対象とする Misskey インスタンスにログインする際にユーザー名およびパスワードを入力する必要があります。
p これらの情報は対象の Misskey インスタンスにログインするためだけに使用されます。本サイト自体は、ユーザーの入力したユーザー名・パスワードを一切収集致しません。
p また、本サイトではユーザーの IP アドレスを収集しています。収集された IP アドレスは、利用規約への違反を行ったユーザーの特定および処分の為に使用されます。
h3 個人情報の第三者への開示
p 悪質な違反行為を行ったユーザーに対する処罰の一環としてプロバイダーへの通報を行う場合や、法令に基づく要請がある場合を除き、収集した個人情報を外部に開示することはありません。また、個人情報の取扱を第三者に委託することもありません。
h3 免責事項
p 本サイトを通じてアクセスする Misskey インスタンスにつきましては、本プライバシーポリシーは適用されません。別途、当該インスタンスのプライバシーポリシーをご確認頂く必要があります。本サイトでは、アクセス先のインスタンスにて取り扱われる個人情報については一切の責任を負いません。
h3 変更について
p 当サイトは、個人情報に関して適用される日本の法令を遵守するとともに、本ポリシーの内容を適宜見直しその改善に努めます。
p 修正された最新のプライバシーポリシーは常に本ページにて開示されます。

8
src/views/terms.pug Normal file
View File

@ -0,0 +1,8 @@
extends _base
block content
h2 利用規約
p 本サイトの利用規約を以下に示します。本サイトのサービスをご利用いただいた時点で、自動的に本規約に同意したものとみなされます。
h3 禁止行為
p 以下に定める行為は固く禁じます。

27
src/views/timeline.pug Normal file
View File

@ -0,0 +1,27 @@
extends _base
block content
+avatar(user)
p: b=getUserName(user)
| &nbsp;
span(style="color: gray")= getAcct(user)
form(action="/note", method="post")
textarea(name="text", placeholder="今何してる?" style="max-width: 100%; min-width: 100%; height: 6em; margin-bottom: 8px")
button(type="submit") ノート
hr
div
a(href="/") ホーム
| ・
a(href="/ltl") ローカル
| ・
a(href="/stl") ソーシャル
| ・
a(href="/gtl") グローバル
h2= timelineName
each note in timeline
if (note.renote)
p: b 🔁 !{getUserName(note.user)} がRenote
+note(note.renote)
else
+note(note)
hr

72
tsconfig.json Normal file
View File

@ -0,0 +1,72 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./built/", /* Redirect output structure to the directory. */
"rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"node_modules/@types",
"src/@types"
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

3018
yarn.lock Normal file

File diff suppressed because it is too large Load Diff