Merge branch 'develop' into feature/edits
|
@ -1,16 +1,48 @@
|
|||
.autogen
|
||||
.github
|
||||
.travis
|
||||
.vscode
|
||||
.config
|
||||
# Visual Studio Code
|
||||
/.vscode
|
||||
!/.vscode/extensions.json
|
||||
|
||||
# Intelij-IDEA
|
||||
/.idea
|
||||
packages/backend/.idea/backend.iml
|
||||
packages/backend/.idea/modules.xml
|
||||
packages/backend/.idea/vcs.xml
|
||||
|
||||
# Node.js
|
||||
node_modules
|
||||
report.*.json
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
||||
# Coverage
|
||||
coverage
|
||||
|
||||
# config
|
||||
/.config/*
|
||||
!/.config/example.yml
|
||||
!/.config/docker_example.env
|
||||
|
||||
#docker dev config
|
||||
/dev/docker-compose.yml
|
||||
|
||||
# misskey
|
||||
built
|
||||
db
|
||||
elasticsearch
|
||||
redis
|
||||
npm-debug.log
|
||||
*.pem
|
||||
run.bat
|
||||
api-docs.json
|
||||
*.log
|
||||
*.code-workspace
|
||||
.DS_Store
|
||||
files
|
||||
ormconfig.json
|
||||
packages/backend/assets/instance.css
|
||||
|
||||
# dockerignore custom
|
||||
.git
|
||||
Dockerfile
|
||||
build/
|
||||
built/
|
||||
db/
|
||||
docker-compose.yml
|
||||
elasticsearch/
|
||||
node_modules/
|
||||
redis/
|
||||
files/
|
||||
misskey-assets/
|
||||
.pnp.*
|
||||
|
|
|
@ -28,24 +28,22 @@ coverage
|
|||
/dev/docker-compose.yml
|
||||
|
||||
# misskey
|
||||
/build
|
||||
built
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
/elasticsearch
|
||||
db
|
||||
elasticsearch
|
||||
redis
|
||||
npm-debug.log
|
||||
*.pem
|
||||
run.bat
|
||||
api-docs.json
|
||||
*.log
|
||||
/redis
|
||||
*.code-workspace
|
||||
.DS_Store
|
||||
/files
|
||||
files
|
||||
ormconfig.json
|
||||
/custom
|
||||
packages/backend/assets/instance.css
|
||||
packages/backend/assets/sounds/None.mp3
|
||||
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
|
|
|
@ -11,10 +11,5 @@ pipeline:
|
|||
password:
|
||||
# Secret 'docker_password' needs to be set in the CI settings
|
||||
from_secret: docker_password
|
||||
when:
|
||||
# Push new version of tag latest if new push on main-branch
|
||||
event: push
|
||||
branch: main
|
||||
|
||||
depends_on:
|
||||
- prSecurityCheck
|
||||
branches: main
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
pipeline:
|
||||
publish-docker-latest:
|
||||
image: plugins/kaniko
|
||||
settings:
|
||||
repo: thatonecalculator/calckey
|
||||
tags: rc
|
||||
dockerfile: Dockerfile
|
||||
username:
|
||||
# Secret 'docker_username' needs to be set in the CI settings
|
||||
from_secret: docker_username
|
||||
password:
|
||||
# Secret 'docker_password' needs to be set in the CI settings
|
||||
from_secret: docker_password
|
||||
|
||||
branches: beta
|
|
@ -17,5 +17,3 @@ pipeline:
|
|||
event: tag
|
||||
tag: v*
|
||||
|
||||
depends_on:
|
||||
- prSecurityCheck
|
||||
|
|
|
@ -8,4 +8,4 @@ pipeline:
|
|||
no_push: true
|
||||
|
||||
branches:
|
||||
include: [ main, develop ]
|
||||
include: [ main, develop, beta ]
|
||||
|
|
10329
CHANGELOG.md
50
Dockerfile
|
@ -1,25 +1,51 @@
|
|||
FROM node:19-alpine
|
||||
ARG NODE_ENV=production
|
||||
## Install dev and compilation dependencies, build files
|
||||
FROM node:19-alpine as build
|
||||
WORKDIR /calckey
|
||||
|
||||
# Copy Files
|
||||
COPY . ./
|
||||
# Install compilation dependencies
|
||||
RUN apk add --no-cache --no-progress git alpine-sdk python3
|
||||
|
||||
# Install Dependencies
|
||||
RUN apk update
|
||||
RUN apk add git ffmpeg tini alpine-sdk python3
|
||||
# Copy only the dependency-related files first, to cache efficiently
|
||||
COPY package.json pnpm*.yaml ./
|
||||
COPY packages/backend/package.json packages/backend/package.json
|
||||
COPY packages/client/package.json packages/client/package.json
|
||||
COPY packages/sw/package.json packages/sw/package.json
|
||||
|
||||
# Configure corepack and pnpm
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@latest --activate
|
||||
RUN pnpm i --frozen-lockfile
|
||||
ARG NODE_ENV=production
|
||||
|
||||
# Build project (pnp dependencies are installed)
|
||||
# Install dev mode dependencies for compilation
|
||||
RUN pnpm i --frozen-lockfile
|
||||
|
||||
# Copy in the rest of the files, to compile from TS to JS
|
||||
COPY . ./
|
||||
RUN pnpm run build
|
||||
|
||||
# Remove git files
|
||||
RUN rm -rf .git
|
||||
# Trim down the dependencies to only the prod deps
|
||||
RUN pnpm i --prod --frozen-lockfile
|
||||
|
||||
|
||||
## Runtime container
|
||||
FROM node:19-alpine
|
||||
WORKDIR /calckey
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache --no-progress tini ffmpeg vips-dev
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Copy node modules
|
||||
COPY --from=build /calckey/node_modules /calckey/node_modules
|
||||
COPY --from=build /calckey/packages/backend/node_modules /calckey/packages/backend/node_modules
|
||||
COPY --from=build /calckey/packages/sw/node_modules /calckey/packages/sw/node_modules
|
||||
COPY --from=build /calckey/packages/client/node_modules /calckey/packages/client/node_modules
|
||||
|
||||
# Copy the finished compiled files
|
||||
COPY --from=build /calckey/built /calckey/built
|
||||
COPY --from=build /calckey/packages/backend/built /calckey/packages/backend/built
|
||||
COPY --from=build /calckey/packages/backend/assets/instance.css /calckey/packages/backend/assets/instance.css
|
||||
|
||||
RUN corepack enable
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
CMD [ "pnpm", "run", "migrateandstart" ]
|
||||
|
|
|
@ -97,9 +97,10 @@ If you have access to a server that supports one of the sources below, I recomme
|
|||
```sh
|
||||
git clone --depth 1 https://codeberg.org/calckey/calckey.git
|
||||
cd calckey/
|
||||
# git checkout main # if you want only stable versions
|
||||
```
|
||||
|
||||
By default, you're on the development branch. Run `git checkout beta` or `git checkout main` to switch to the Beta/Main branches.
|
||||
|
||||
## 📩 Install dependencies
|
||||
|
||||
```sh
|
||||
|
@ -123,6 +124,8 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
|
|||
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
|
||||
- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`.
|
||||
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
|
||||
- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
|
||||
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
|
||||
- To update custom assets without rebuilding, just run `pnpm run gulp`.
|
||||
|
||||
## 🧑🔬 Configuring a new instance
|
||||
|
|
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 56 KiB |
|
@ -15,8 +15,9 @@ gulp.task('copy:backend:views', () =>
|
|||
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
|
||||
);
|
||||
|
||||
|
||||
gulp.task('copy:backend:custom', () =>
|
||||
gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/'))
|
||||
gulp.src('./custom/assets/**/*').pipe(gulp.dest('./packages/backend/assets/'))
|
||||
);
|
||||
|
||||
gulp.task('copy:client:fonts', () =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
_lang_: "Deutsch"
|
||||
headlineMisskey: "Ein durch Notizen verbundenes Netzwerk"
|
||||
introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nEine neue Welt wartet auf dich! 🚀"
|
||||
headlineMisskey: "Ein durch Posts verbundenes Netzwerk"
|
||||
introMisskey: "Willkommen! Calckey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Posts“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Posts anderer Benutzer zum Ausdruck bringen. 👍\nEine neue Welt wartet auf dich! 🚀"
|
||||
monthAndDay: "{day}.{month}."
|
||||
search: "Suchen"
|
||||
notifications: "Benachrichtigungen"
|
||||
|
|
|
@ -32,6 +32,7 @@ uploading: "Uploading..."
|
|||
save: "Save"
|
||||
users: "Users"
|
||||
addUser: "Add a user"
|
||||
addInstance: "Add an instance"
|
||||
favorite: "Add to bookmarks"
|
||||
favorites: "Bookmarks"
|
||||
unfavorite: "Remove from bookmarks"
|
||||
|
@ -160,6 +161,7 @@ proxyAccount: "Proxy account"
|
|||
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
|
||||
host: "Host"
|
||||
selectUser: "Select a user"
|
||||
selectInstance: "Select an instance"
|
||||
recipient: "Recipient(s)"
|
||||
annotation: "Comments"
|
||||
federation: "Federation"
|
||||
|
@ -197,6 +199,7 @@ muteAndBlock: "Mutes and Blocks"
|
|||
mutedUsers: "Muted users"
|
||||
blockedUsers: "Blocked users"
|
||||
noUsers: "There are no users"
|
||||
noInstances: "There are no instances"
|
||||
editProfile: "Edit profile"
|
||||
noteDeleteConfirm: "Are you sure you want to delete this post?"
|
||||
pinLimitExceeded: "You cannot pin any more posts"
|
||||
|
@ -363,6 +366,7 @@ notifyAntenna: "Notify about new posts"
|
|||
withFileAntenna: "Only posts with files"
|
||||
enableServiceworker: "Enable Push-Notifications for your Browser"
|
||||
antennaUsersDescription: "List one username per line"
|
||||
antennaInstancesDescription: "List one instance host per line"
|
||||
caseSensitive: "Case sensitive"
|
||||
withReplies: "Include replies"
|
||||
connectedTo: "Following account(s) are connected"
|
||||
|
@ -816,6 +820,7 @@ lastCommunication: "Last communication"
|
|||
resolved: "Resolved"
|
||||
unresolved: "Unresolved"
|
||||
breakFollow: "Remove follower"
|
||||
breakFollowConfirm: "Are you sure want to remove follower?"
|
||||
itsOn: "Enabled"
|
||||
itsOff: "Disabled"
|
||||
emailRequiredForSignup: "Require email address for sign-up"
|
||||
|
@ -1293,12 +1298,14 @@ _auth:
|
|||
pleaseGoBack: "Please go back to the application"
|
||||
callback: "Returning to the application"
|
||||
denied: "Access denied"
|
||||
copyAsk: "Please paste the following authorization code to the application"
|
||||
_antennaSources:
|
||||
all: "All posts"
|
||||
homeTimeline: "Posts from followed users"
|
||||
users: "Posts from specific users"
|
||||
userList: "Posts from a specified list of users"
|
||||
userGroup: "Posts from users in a specified group"
|
||||
instances: "Posts from all users on an instance"
|
||||
_weekday:
|
||||
sunday: "Sunday"
|
||||
monday: "Monday"
|
||||
|
@ -1393,6 +1400,7 @@ _profile:
|
|||
metadataContent: "Content"
|
||||
changeAvatar: "Change avatar"
|
||||
changeBanner: "Change banner"
|
||||
locationDescription: "If entered properly, this will display your local time to other users."
|
||||
_exportOrImport:
|
||||
allNotes: "All posts"
|
||||
followingList: "Followed users"
|
||||
|
@ -1785,3 +1793,20 @@ _deck:
|
|||
list: "List"
|
||||
mentions: "Mentions"
|
||||
direct: "Direct messages"
|
||||
_apps:
|
||||
apps: "Apps"
|
||||
crossPlatform: "Cross platform"
|
||||
mobile: "Mobile"
|
||||
firstParty: "First party"
|
||||
firstClass: "First class"
|
||||
secondClass: "Second class"
|
||||
thirdClass: "Third class"
|
||||
free: "Free"
|
||||
paid: "Paid"
|
||||
pwa: "Install PWA"
|
||||
kaiteki: "Kaiteki"
|
||||
milktea: "Milktea"
|
||||
subwayTooter: "Subway Tooter"
|
||||
kimis: "Kimis"
|
||||
theDesk: "TheDesk"
|
||||
lesskey: "Lesskey"
|
||||
|
|
|
@ -816,6 +816,7 @@ lastCommunication: "直近の通信"
|
|||
resolved: "解決済み"
|
||||
unresolved: "未解決"
|
||||
breakFollow: "フォロワーを解除"
|
||||
breakFollowConfirm: "フォロワー解除しますか?"
|
||||
itsOn: "オンになっています"
|
||||
itsOff: "オフになっています"
|
||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||
|
|
23
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "13.1.0",
|
||||
"version": "13.2.0-dev11",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "pnpm@7.26.3",
|
||||
"packageManager": "pnpm@7.27.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
||||
|
@ -20,6 +20,7 @@
|
|||
"gulp": "gulp build",
|
||||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.js",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r run lint",
|
||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "cypress run",
|
||||
|
@ -32,22 +33,14 @@
|
|||
"cleanall": "pnpm run clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
"chokidar": "^3.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.10.2",
|
||||
"@bull-board/ui": "^4.10.2",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"calckey-js": "^0.0.20",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"calckey-js": "^0.0.22",
|
||||
"js-yaml": "4.1.0",
|
||||
"long": "^5.2.1",
|
||||
"phosphor-icons": "^1.4.2",
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
|
@ -56,6 +49,12 @@
|
|||
"@types/gulp-rename": "2.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"rome": "^11.0.0",
|
||||
"start-server-and-test": "1.15.2",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<svg id="svg10" version="1.1" sodipodi:docname="title_float.svg" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="1.95 0.97 167.97 103.23">
|
||||
<sodipodi:namedview id="namedview21" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showgrid="false" inkscape:zoom="1.1507704" inkscape:cx="260.69492" inkscape:cy="102.54" inkscape:window-width="1600" inkscape:window-height="931" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg10"/>
|
||||
<metadata id="metadata16">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<linearGradient id="myGradient" gradientTransform="rotate(90)">
|
||||
<stop offset="5%" stop-color="#9ccfd8" id="stop5" style="--darkreader-inline-stopcolor: #265760;" data-darkreader-inline-stopcolor=""/>
|
||||
<stop offset="95%" stop-color="#31748f" id="stop7" style="--darkreader-inline-stopcolor: #275d72;" data-darkreader-inline-stopcolor=""/>
|
||||
</linearGradient>
|
||||
<defs id="defs14"/>
|
||||
<g id="g8" fill="url('#myGradient')" word-spacing="0" letter-spacing="0" font-family="OTADESIGN Rounded" font-weight="400">
|
||||
<g id="g17">
|
||||
<g transform="matrix(.26953 0 0 .26953 -55.341 -52.023)" id="g11"/>
|
||||
<g transform="matrix(3.6954 0 0 3.6954 208.34 -284.25)" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" id="g15">
|
||||
<path d="m -41.8312,77.19 c -3.8683,0 -7.1782,1.3578 -9.9311,4.0734 -2.716,2.7525 -4.0734,6.0628 -4.0734,9.9311 5.0539,0.04979 6.082,0.01348 8.7525,0.0011 0.0024,-4.51e-4 0.0044,-6.05e-4 0.0069,-0.0011 0.03779,-2.8423 2.2103,-4.9346 5.2451,-5.2451 1.4137,0 2.6227,0.52089 3.6268,1.5629 0.855,0.8548 1.897,1.2822 3.1257,1.2822 1.2258,0 2.2676,-0.42741 3.1236,-1.2822 0.85499,-0.85567 1.2833,-1.8976 1.2833,-3.1257 0,-1.2264 -0.42828,-2.2673 -1.2833,-3.1231 -2.7528,-2.7156 -6.0446,-4.0734 -9.8761,-4.0734 z m -5.252,14.006 c -3.4453,-5.5934 -3.4667,0.08539 -8.7525,-0.0011 0,3.8683 1.3584,7.0406 4.0744,9.7931 2.7528,2.7156 6.0623,4.0734 9.9305,4.0734 3.8315,0 7.1238,-1.3578 9.8766,-4.0734 0.85499,-0.85577 1.2827,-1.8967 1.2827,-3.1231 0,-1.2282 -0.42775,-2.2701 -1.2827,-3.1257 -0.85596,-0.8548 -1.8978,-1.2822 -3.1236,-1.2822 -1.2287,0 -2.2707,0.42741 -3.1257,1.2822 -1.0041,1.042 -2.2136,1.5623 -3.6273,1.5623 -3.0348,-0.31051 -5.2084,-2.2633 -5.2462,-5.1056 -0.0024,1.1e-5 -0.0039,-1.2e-5 -0.0063,0 z m 26.154,-7.0965 c -2.8795,0 -5.3538,1.0204 -7.4227,3.0612 -0.64257,0.64316 -0.96404,1.4255 -0.96404,2.3472 0,0.92303 0.32146,1.7062 0.96404,2.3493 0.64331,0.64243 1.4265,0.96351 2.3477,0.96351 0.92347,0 1.7068,-0.32108 2.3493,-0.96351 0.75465,-0.78308 1.6632,-1.1744 2.7256,-1.1744 1.0894,0 2.0261,0.37695 2.8091,1.1316 0.75536,0.78309 1.1332,1.7201 1.1332,2.8102 0,1.0617 -0.39242,1.9703 -1.1754,2.7256 -0.39149,0.4193 -0.86676,0.69884 -1.4249,0.83878 -0.14116,0.02773 -0.25248,0.01369 -0.33614,-0.04175 -0.05605,-0.08456 -0.02751,-0.16827 0.08456,-0.25211 l 0.83825,-0.88053 c 0.64329,-0.64315 0.9651,-1.412 0.9651,-2.306 0,-0.92236 -0.27937,-1.6632 -0.83825,-2.2225 -0.55888,-0.55932 -1.3422,-0.83878 -2.3493,-0.83878 -0.6986,0 -1.397,0.34902 -2.0956,1.0475 l -4.8651,4.8223 c -0.64328,0.6438 -0.96457,1.4271 -0.96457,2.3488 0,0.92302 0.32128,1.7053 0.96457,2.3477 1.9568,1.9293 4.3751,2.8942 7.2546,2.8942 2.9072,0 5.3945,-1.0343 7.4634,-3.103 2.0412,-2.0409 3.0618,-4.501 3.0618,-7.3804 0,-2.9072 -1.0206,-5.3952 -3.0618,-7.4639 -2.0689,-2.0409 -4.5562,-3.0612 -7.4634,-3.0612 z" clip-rule="evenodd" fill-rule="nonzero" stroke-miterlimit="2" stroke-width="0" id="path13" sodipodi:nodetypes="cccccccscsccccccscscscccccccscscscscccccscsccscscsccc"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512.0px"
|
||||
height="512.0px"
|
||||
viewBox="0 0 512.0 512.0"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="inverse wordmark.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview71"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="px"
|
||||
showgrid="true"
|
||||
inkscape:zoom="0.50150542"
|
||||
inkscape:cx="59.819892"
|
||||
inkscape:cy="189.42966"
|
||||
inkscape:window-width="1600"
|
||||
inkscape:window-height="931"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"><inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid77" /></sodipodi:namedview><defs
|
||||
id="defs66" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"><path
|
||||
id="rect136"
|
||||
style="fill:#31748f;fill-opacity:1;stroke-width:0.996356"
|
||||
d="M 0,0 V 512 H 512 V 0 Z" /><g
|
||||
id="g17"
|
||||
style="font-weight:400;font-family:'OTADESIGN Rounded';letter-spacing:0;word-spacing:0;fill:#ffffff"
|
||||
transform="matrix(2.0847185,0,0,2.0847185,76.820648,146.38203)"><g
|
||||
transform="matrix(0.26953,0,0,0.26953,-55.341,-52.023)"
|
||||
id="g11"
|
||||
style="fill:#ffffff" /><g
|
||||
transform="matrix(3.6954,0,0,3.6954,208.34,-284.25)"
|
||||
clip-rule="evenodd"
|
||||
fill-rule="evenodd"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="2"
|
||||
id="g15"
|
||||
style="fill:#ffffff"><path
|
||||
d="m -41.8312,77.19 c -3.8683,0 -7.1782,1.3578 -9.9311,4.0734 -2.716,2.7525 -4.0734,6.0628 -4.0734,9.9311 5.0539,0.04979 6.082,0.01348 8.7525,0.0011 0.0024,-4.51e-4 0.0044,-6.05e-4 0.0069,-0.0011 0.03779,-2.8423 2.2103,-4.9346 5.2451,-5.2451 1.4137,0 2.6227,0.52089 3.6268,1.5629 0.855,0.8548 1.897,1.2822 3.1257,1.2822 1.2258,0 2.2676,-0.42741 3.1236,-1.2822 0.85499,-0.85567 1.2833,-1.8976 1.2833,-3.1257 0,-1.2264 -0.42828,-2.2673 -1.2833,-3.1231 -2.7528,-2.7156 -6.0446,-4.0734 -9.8761,-4.0734 z m -5.252,14.006 c -3.4453,-5.5934 -3.4667,0.08539 -8.7525,-0.0011 0,3.8683 1.3584,7.0406 4.0744,9.7931 2.7528,2.7156 6.0623,4.0734 9.9305,4.0734 3.8315,0 7.1238,-1.3578 9.8766,-4.0734 0.85499,-0.85577 1.2827,-1.8967 1.2827,-3.1231 0,-1.2282 -0.42775,-2.2701 -1.2827,-3.1257 -0.85596,-0.8548 -1.8978,-1.2822 -3.1236,-1.2822 -1.2287,0 -2.2707,0.42741 -3.1257,1.2822 -1.0041,1.042 -2.2136,1.5623 -3.6273,1.5623 -3.0348,-0.31051 -5.2084,-2.2633 -5.2462,-5.1056 -0.0024,1.1e-5 -0.0039,-1.2e-5 -0.0063,0 z m 26.154,-7.0965 c -2.8795,0 -5.3538,1.0204 -7.4227,3.0612 -0.64257,0.64316 -0.96404,1.4255 -0.96404,2.3472 0,0.92303 0.32146,1.7062 0.96404,2.3493 0.64331,0.64243 1.4265,0.96351 2.3477,0.96351 0.92347,0 1.7068,-0.32108 2.3493,-0.96351 0.75465,-0.78308 1.6632,-1.1744 2.7256,-1.1744 1.0894,0 2.0261,0.37695 2.8091,1.1316 0.75536,0.78309 1.1332,1.7201 1.1332,2.8102 0,1.0617 -0.39242,1.9703 -1.1754,2.7256 -0.39149,0.4193 -0.86676,0.69884 -1.4249,0.83878 -0.14116,0.02773 -0.25248,0.01369 -0.33614,-0.04175 -0.05605,-0.08456 -0.02751,-0.16827 0.08456,-0.25211 l 0.83825,-0.88053 c 0.64329,-0.64315 0.9651,-1.412 0.9651,-2.306 0,-0.92236 -0.27937,-1.6632 -0.83825,-2.2225 -0.55888,-0.55932 -1.3422,-0.83878 -2.3493,-0.83878 -0.6986,0 -1.397,0.34902 -2.0956,1.0475 l -4.8651,4.8223 c -0.64328,0.6438 -0.96457,1.4271 -0.96457,2.3488 0,0.92302 0.32128,1.7053 0.96457,2.3477 1.9568,1.9293 4.3751,2.8942 7.2546,2.8942 2.9072,0 5.3945,-1.0343 7.4634,-3.103 2.0412,-2.0409 3.0618,-4.501 3.0618,-7.3804 0,-2.9072 -1.0206,-5.3952 -3.0618,-7.4639 -2.0689,-2.0409 -4.5562,-3.0612 -7.4634,-3.0612 z"
|
||||
clip-rule="evenodd"
|
||||
fill-rule="nonzero"
|
||||
stroke-miterlimit="2"
|
||||
stroke-width="0"
|
||||
id="path13"
|
||||
sodipodi:nodetypes="cccccccscsccccccscscscccccccscscscscccccscsccscscsccc"
|
||||
style="fill:#ffffff" /></g></g></g></svg>
|
After Width: | Height: | Size: 4.3 KiB |
|
@ -0,0 +1,17 @@
|
|||
export class AntennaInstances1676093997212 {
|
||||
name = 'AntennaInstances1676093997212'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'instances'`);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "instances" jsonb NOT NULL DEFAULT '[]'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DELETE FROM "antenna" WHERE "src" = 'instances'`);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "instances"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list', 'group')`);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum_old" USING "src"::"text"::"public"."antenna_src_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."antenna_src_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum_old" RENAME TO "antenna_src_enum"`);
|
||||
}
|
||||
}
|
|
@ -15,8 +15,7 @@
|
|||
"test": "pnpm run mocha"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
"chokidar": "^3.3.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
|
@ -34,20 +33,21 @@
|
|||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@swc/cli": "^0.1.59",
|
||||
"@swc/core": "^1.3.26",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@tensorflow/tfjs": "^4.2.0",
|
||||
"ajv": "8.11.2",
|
||||
"archiver": "5.3.1",
|
||||
"koa-body": "^6.0.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autolinker": "4.0.0",
|
||||
"axios": "^1.3.2",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1277.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"bull": "4.10.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"calckey-js": "^0.0.20",
|
||||
"calckey-js": "^0.0.22",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.2.0",
|
||||
"chalk-template": "0.4.0",
|
||||
|
@ -68,8 +68,6 @@
|
|||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "20.0.3",
|
||||
"json5": "2.2.3",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "6.0.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"koa": "2.13.4",
|
||||
|
@ -81,9 +79,9 @@
|
|||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"@calckey/megalodon": "5.1.2",
|
||||
"mfm-js": "0.23.2",
|
||||
"mime-types": "2.1.35",
|
||||
"mocha": "10.2.0",
|
||||
"multer": "1.4.4-lts.1",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.0",
|
||||
|
@ -96,7 +94,6 @@
|
|||
"private-ip": "2.3.4",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.1.1",
|
||||
"pureimage": "0.3.15",
|
||||
"qrcode": "1.5.1",
|
||||
|
@ -108,13 +105,11 @@
|
|||
"rename": "1.0.4",
|
||||
"rndstr": "1.0.0",
|
||||
"rss-parser": "3.12.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.8.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.31.3",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
|
@ -122,9 +117,6 @@
|
|||
"tesseract.js": "^3.0.3",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"ulid": "2.3.0",
|
||||
|
@ -132,10 +124,11 @@
|
|||
"uuid": "9.0.0",
|
||||
"web-push": "3.5.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.11.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.59",
|
||||
"@swc/core": "^1.3.26",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
|
@ -179,11 +172,21 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.31.0",
|
||||
"execa": "6.1.0",
|
||||
"json5": "2.2.3",
|
||||
"json5-loader": "4.0.1",
|
||||
"mocha": "10.2.0",
|
||||
"pug": "3.0.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"typescript": "4.9.4",
|
||||
"webpack": "^5.75.0"
|
||||
"webpack": "^5.75.0",
|
||||
"ws": "8.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,13 @@ export async function checkHitAntenna(
|
|||
)
|
||||
)
|
||||
return false;
|
||||
} else if (antenna.src === "instances") {
|
||||
const instances = antenna.instances
|
||||
.filter((x) => x !== "")
|
||||
.map((host) => {
|
||||
return host.toLowerCase();
|
||||
});
|
||||
if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false;
|
||||
}
|
||||
|
||||
const keywords = antenna.keywords
|
||||
|
|
|
@ -2,3 +2,4 @@ import twemoji from "twemoji-parser/dist/lib/regex.js";
|
|||
const twemojiRegex = twemoji.default;
|
||||
|
||||
export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
|
||||
export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`);
|
||||
|
|
|
@ -11,26 +11,41 @@ const size = 128; // px
|
|||
const n = 5; // resolution
|
||||
const margin = size / 4;
|
||||
const colors = [
|
||||
["#FF512F", "#DD2476"],
|
||||
["#FF61D2", "#FE9090"],
|
||||
["#72FFB6", "#10D164"],
|
||||
["#FD8451", "#FFBD6F"],
|
||||
["#305170", "#6DFC6B"],
|
||||
["#00C0FF", "#4218B8"],
|
||||
["#009245", "#FCEE21"],
|
||||
["#0100EC", "#FB36F4"],
|
||||
["#FDABDD", "#374A5A"],
|
||||
["#38A2D7", "#561139"],
|
||||
["#121C84", "#8278DA"],
|
||||
["#5761B2", "#1FC5A8"],
|
||||
["#FFDB01", "#0E197D"],
|
||||
["#FF3E9D", "#0E1F40"],
|
||||
["#766eff", "#00d4ff"],
|
||||
["#9bff6e", "#00d4ff"],
|
||||
["#ff6e94", "#00d4ff"],
|
||||
["#ffa96e", "#00d4ff"],
|
||||
["#ffa96e", "#ff009d"],
|
||||
["#ffdd6e", "#ff009d"],
|
||||
["#eb6f92", "#b4637a"],
|
||||
["#f6c177", "#ea9d34"],
|
||||
["#ebbcba", "#d7827e"],
|
||||
["#9ccfd8", "#56949f"],
|
||||
["#c4a7e7", "#907aa9"],
|
||||
["#eb6f92", "#f6c177"],
|
||||
["#eb6f92", "#ebbcba"],
|
||||
["#eb6f92", "#31748f"],
|
||||
["#eb6f92", "#9ccfd8"],
|
||||
["#eb6f92", "#c4a7e7"],
|
||||
["#f6c177", "#eb6f92"],
|
||||
["#f6c177", "#ebbcba"],
|
||||
["#f6c177", "#31748f"],
|
||||
["#f6c177", "#9ccfd8"],
|
||||
["#f6c177", "#c4a7e7"],
|
||||
["#ebbcba", "#eb6f92"],
|
||||
["#ebbcba", "#f6c177"],
|
||||
["#ebbcba", "#31748f"],
|
||||
["#ebbcba", "#9ccfd8"],
|
||||
["#ebbcba", "#c4a7e7"],
|
||||
["#31748f", "#eb6f92"],
|
||||
["#31748f", "#f6c177"],
|
||||
["#31748f", "#ebbcba"],
|
||||
["#31748f", "#9ccfd8"],
|
||||
["#31748f", "#c4a7e7"],
|
||||
["#9ccfd8", "#eb6f92"],
|
||||
["#9ccfd8", "#f6c177"],
|
||||
["#9ccfd8", "#ebbcba"],
|
||||
["#9ccfd8", "#31748f"],
|
||||
["#9ccfd8", "#c4a7e7"],
|
||||
["#c4a7e7", "#eb6f92"],
|
||||
["#c4a7e7", "#f6c177"],
|
||||
["#c4a7e7", "#ebbcba"],
|
||||
["#c4a7e7", "#31748f"],
|
||||
["#c4a7e7", "#9ccfd8"],
|
||||
];
|
||||
|
||||
const actualSize = size - margin * 2;
|
||||
|
|
|
@ -2,9 +2,9 @@ export function nyaize(text: string): string {
|
|||
return (
|
||||
text
|
||||
// ja-JP
|
||||
.replace(/な/g, "にゃ")
|
||||
.replace(/ナ/g, "ニャ")
|
||||
.replace(/ナ/g, "ニャ")
|
||||
.replaceAll("な", "にゃ")
|
||||
.replaceAll("ナ", "ニャ")
|
||||
.replaceAll("ナ", "ニャ")
|
||||
// en-US
|
||||
.replace(/(?<=n)a/gi, (x) => (x === "A" ? "YA" : "ya"))
|
||||
.replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan"))
|
||||
|
|
|
@ -4,78 +4,84 @@ import { Emojis } from "@/models/index.js";
|
|||
import { toPunyNullable } from "./convert-host.js";
|
||||
import { IsNull } from "typeorm";
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
like: "👍",
|
||||
love: "❤️", // ここに記述する場合は異体字セレクタを入れない <- not that good because modern browsers just display it as the red heart so just convert it to it to not end up with two seperate reactions of "the same emoji" for the user
|
||||
laugh: "😆",
|
||||
hmm: "🤔",
|
||||
surprise: "😮",
|
||||
congrats: "🎉",
|
||||
angry: "💢",
|
||||
confused: "😥",
|
||||
rip: "😇",
|
||||
pudding: "🍮",
|
||||
star: "⭐",
|
||||
};
|
||||
const legacies = new Map([
|
||||
["like", "👍"],
|
||||
["love", "❤️"],
|
||||
["laugh", "😆"],
|
||||
["hmm", "🤔"],
|
||||
["surprise", "😮"],
|
||||
["congrats", "🎉"],
|
||||
["angry", "💢"],
|
||||
["confused", "😥"],
|
||||
["rip", "😇"],
|
||||
["pudding", "🍮"],
|
||||
["star", "⭐"],
|
||||
]);
|
||||
|
||||
export async function getFallbackReaction(): Promise<string> {
|
||||
export async function getFallbackReaction() {
|
||||
const meta = await fetchMeta();
|
||||
return meta.defaultReaction;
|
||||
}
|
||||
|
||||
export function convertLegacyReactions(reactions: Record<string, number>) {
|
||||
const _reactions = {} as Record<string, number>;
|
||||
const _reactions = new Map();
|
||||
const decodedReactions = new Map();
|
||||
|
||||
for (const reaction of Object.keys(reactions)) {
|
||||
for (const reaction in reactions) {
|
||||
if (reactions[reaction] <= 0) continue;
|
||||
|
||||
if (Object.keys(legacies).includes(reaction)) {
|
||||
if (_reactions[legacies[reaction]]) {
|
||||
_reactions[legacies[reaction]] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[legacies[reaction]] = reactions[reaction];
|
||||
}
|
||||
let decodedReaction;
|
||||
if (decodedReactions.has(reaction)) {
|
||||
decodedReaction = decodedReactions.get(reaction);
|
||||
} else {
|
||||
if (_reactions[reaction]) {
|
||||
_reactions[reaction] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[reaction] = reactions[reaction];
|
||||
}
|
||||
decodedReaction = decodeReaction(reaction);
|
||||
decodedReactions.set(reaction, decodedReaction);
|
||||
}
|
||||
|
||||
let emoji = legacies.get(decodedReaction.reaction);
|
||||
if (emoji) {
|
||||
_reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]);
|
||||
} else {
|
||||
_reactions.set(
|
||||
reaction,
|
||||
(_reactions.get(reaction) || 0) + reactions[reaction],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _reactions2 = {} as Record<string, number>;
|
||||
|
||||
for (const reaction of Object.keys(_reactions)) {
|
||||
_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction];
|
||||
const _reactions2 = new Map();
|
||||
for (const [reaction, count] of _reactions) {
|
||||
const decodedReaction = decodedReactions.get(reaction);
|
||||
_reactions2.set(decodedReaction.reaction, count);
|
||||
}
|
||||
|
||||
return _reactions2;
|
||||
return Object.fromEntries(_reactions2);
|
||||
}
|
||||
|
||||
export async function toDbReaction(
|
||||
reaction?: string | null,
|
||||
reacterHost?: string | null,
|
||||
): Promise<string> {
|
||||
if (reaction == null) return await getFallbackReaction();
|
||||
if (!reaction) return await getFallbackReaction();
|
||||
|
||||
reacterHost = toPunyNullable(reacterHost);
|
||||
|
||||
// 文字列タイプのリアクションを絵文字に変換
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
// Convert string-type reactions to unicode
|
||||
const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null);
|
||||
if (emoji) return emoji;
|
||||
|
||||
// Unicode絵文字
|
||||
// Allow unicode reactions
|
||||
const match = emojiRegex.exec(reaction);
|
||||
if (match) {
|
||||
const unicode = match[0];
|
||||
return unicode.match("\u200d") ? unicode : unicode.replace(/\ufe0f/g, "");
|
||||
return unicode;
|
||||
}
|
||||
|
||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await Emojis.findOneBy({
|
||||
host: reacterHost ?? IsNull(),
|
||||
host: reacterHost || IsNull(),
|
||||
name,
|
||||
});
|
||||
|
||||
|
@ -124,7 +130,7 @@ export function decodeReaction(str: string): DecodedReaction {
|
|||
}
|
||||
|
||||
export function convertLegacyReaction(reaction: string): string {
|
||||
reaction = decodeReaction(reaction).reaction;
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
return reaction;
|
||||
const decoded = decodeReaction(reaction).reaction;
|
||||
if (legacies.has(decoded)) return legacies.get(decoded)!;
|
||||
return decoded;
|
||||
}
|
||||
|
|
|
@ -40,8 +40,8 @@ export class Antenna {
|
|||
})
|
||||
public name: string;
|
||||
|
||||
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
|
||||
public src: "home" | "all" | "users" | "list" | "group";
|
||||
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group', 'instances'] })
|
||||
public src: "home" | "all" | "users" | "list" | "group" | "instances";
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
|
@ -73,6 +73,11 @@ export class Antenna {
|
|||
})
|
||||
public users: string[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public instances: string[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
|
|
|
@ -25,6 +25,7 @@ export const AntennaRepository = db.getRepository(Antenna).extend({
|
|||
userListId: antenna.userListId,
|
||||
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
|
||||
users: antenna.users,
|
||||
instances: antenna.instances,
|
||||
caseSensitive: antenna.caseSensitive,
|
||||
notify: antenna.notify,
|
||||
withReplies: antenna.withReplies,
|
||||
|
|
|
@ -197,6 +197,11 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
.map((x) => decodeReaction(x).reaction)
|
||||
.map((x) => x.replace(/:/g, ""));
|
||||
|
||||
const noteEmoji = await populateEmojis(
|
||||
note.emojis.concat(reactionEmojiNames),
|
||||
host,
|
||||
);
|
||||
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
|
||||
const packed: Packed<"Note"> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
|
@ -213,8 +218,9 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactions: convertLegacyReactions(note.reactions),
|
||||
reactionEmojis: reactionEmoji,
|
||||
emojis: noteEmoji,
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
|
||||
fileIds: note.fileIds,
|
||||
files: DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
|
|
|
@ -52,7 +52,7 @@ export const packedAntennaSchema = {
|
|||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
enum: ["home", "all", "users", "list", "group"],
|
||||
enum: ["home", "all", "users", "list", "group", "instances"],
|
||||
},
|
||||
userListId: {
|
||||
type: "string",
|
||||
|
@ -76,6 +76,16 @@ export const packedAntennaSchema = {
|
|||
nullable: false,
|
||||
},
|
||||
},
|
||||
instances: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
caseSensitive: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
|
|
|
@ -161,26 +161,9 @@ export const packedNoteSchema = {
|
|||
nullable: false,
|
||||
},
|
||||
emojis: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
reactions: {
|
||||
type: "object",
|
||||
|
|
|
@ -111,10 +111,37 @@ export async function createNote(
|
|||
|
||||
const note: IPost = object;
|
||||
|
||||
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
if (note.id && !note.id.startsWith("https://")) {
|
||||
throw new Error(`unexpected shcema of note.id: ${note.id}`);
|
||||
}
|
||||
|
||||
const url = getOneApHrefNullable(note.url);
|
||||
|
||||
if (url && !url.startsWith("https://")) {
|
||||
throw new Error(`unexpected shcema of note url: ${url}`);
|
||||
}
|
||||
|
||||
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
// Skip if note is made before 2007 (1yr before Fedi was created)
|
||||
// OR skip if note is made 3 days in advance
|
||||
if (note.published) {
|
||||
const DateChecker = new Date(note.published);
|
||||
const FutureCheck = new Date();
|
||||
FutureCheck.setDate(FutureCheck.getDate() + 3); // Allow some wiggle room for misconfigured hosts
|
||||
if (DateChecker.getFullYear() < 2007) {
|
||||
logger.warn(
|
||||
"Note somehow made before Activitypub was created; discarding",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (DateChecker > FutureCheck) {
|
||||
logger.warn("Note somehow made after today; discarding");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch author
|
||||
const actor = (await resolvePerson(
|
||||
getOneApId(note.attributedTo),
|
||||
|
@ -123,7 +150,10 @@ export async function createNote(
|
|||
|
||||
// Skip if author is suspended.
|
||||
if (actor.isSuspended) {
|
||||
throw new Error("actor has been suspended");
|
||||
logger.debug(
|
||||
`User ${actor.usernameLower}@${actor.host} suspended; discarding.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const noteAudience = await parseAudience(actor, note.to, note.cc);
|
||||
|
@ -344,7 +374,7 @@ export async function createNote(
|
|||
apEmojis,
|
||||
poll,
|
||||
uri: note.id,
|
||||
url: getOneApHrefNullable(note.url),
|
||||
url: url,
|
||||
},
|
||||
silent,
|
||||
);
|
||||
|
|
|
@ -195,6 +195,12 @@ export async function createPerson(
|
|||
|
||||
const bday = person["vcard:bday"]?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith("https://")) {
|
||||
throw new Error(`unexpected shcema of person url: ${url}`);
|
||||
}
|
||||
|
||||
// Create user
|
||||
let user: IRemoteUser;
|
||||
try {
|
||||
|
@ -237,7 +243,7 @@ export async function createPerson(
|
|||
description: person.summary
|
||||
? htmlToMfm(truncate(person.summary, summaryLength), person.tag)
|
||||
: null,
|
||||
url: getOneApHrefNullable(person.url),
|
||||
url: url,
|
||||
fields,
|
||||
birthday: bday ? bday[0] : null,
|
||||
location: person["vcard:Address"] || null,
|
||||
|
@ -387,6 +393,12 @@ export async function updatePerson(
|
|||
|
||||
const bday = person["vcard:bday"]?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !url.startsWith("https://")) {
|
||||
throw new Error(`unexpected shcema of person url: ${url}`);
|
||||
}
|
||||
|
||||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
|
@ -430,7 +442,7 @@ export async function updatePerson(
|
|||
await UserProfiles.update(
|
||||
{ userId: exist.id },
|
||||
{
|
||||
url: getOneApHrefNullable(person.url),
|
||||
url: url,
|
||||
fields,
|
||||
description: person.summary
|
||||
? htmlToMfm(truncate(person.summary, summaryLength), person.tag)
|
||||
|
|
|
@ -107,6 +107,7 @@ export async function signup(opts: {
|
|||
isAdmin:
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
})) === 0,
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -198,6 +198,7 @@ import * as ep___i_readAnnouncement from "./endpoints/i/read-announcement.js";
|
|||
import * as ep___i_regenerateToken from "./endpoints/i/regenerate-token.js";
|
||||
import * as ep___i_registry_getAll from "./endpoints/i/registry/get-all.js";
|
||||
import * as ep___i_registry_getDetail from "./endpoints/i/registry/get-detail.js";
|
||||
import * as ep___i_registry_getUnsecure from "./endpoints/i/registry/get-unsecure.js";
|
||||
import * as ep___i_registry_get from "./endpoints/i/registry/get.js";
|
||||
import * as ep___i_registry_keysWithType from "./endpoints/i/registry/keys-with-type.js";
|
||||
import * as ep___i_registry_keys from "./endpoints/i/registry/keys.js";
|
||||
|
@ -221,6 +222,7 @@ import * as ep___messaging_messages_create from "./endpoints/messaging/messages/
|
|||
import * as ep___messaging_messages_delete from "./endpoints/messaging/messages/delete.js";
|
||||
import * as ep___messaging_messages_read from "./endpoints/messaging/messages/read.js";
|
||||
import * as ep___meta from "./endpoints/meta.js";
|
||||
import * as ep___sounds from "./endpoints/get-sounds.js";
|
||||
import * as ep___miauth_genToken from "./endpoints/miauth/gen-token.js";
|
||||
import * as ep___mute_create from "./endpoints/mute/create.js";
|
||||
import * as ep___mute_delete from "./endpoints/mute/delete.js";
|
||||
|
@ -538,6 +540,7 @@ const eps = [
|
|||
["i/regenerate-token", ep___i_regenerateToken],
|
||||
["i/registry/get-all", ep___i_registry_getAll],
|
||||
["i/registry/get-detail", ep___i_registry_getDetail],
|
||||
["i/registry/get-unsecure", ep___i_registry_getUnsecure],
|
||||
["i/registry/get", ep___i_registry_get],
|
||||
["i/registry/keys-with-type", ep___i_registry_keysWithType],
|
||||
["i/registry/keys", ep___i_registry_keys],
|
||||
|
@ -666,6 +669,7 @@ const eps = [
|
|||
["users/stats", ep___users_stats],
|
||||
["admin/drive-capacity-override", ep___admin_driveCapOverride],
|
||||
["fetch-rss", ep___fetchRss],
|
||||
["get-sounds", ep___sounds],
|
||||
];
|
||||
|
||||
export interface IEndpointMeta {
|
||||
|
@ -766,16 +770,16 @@ export interface IEndpointMeta {
|
|||
|
||||
export interface IEndpoint {
|
||||
name: string;
|
||||
exec: any;
|
||||
exec: any; // TODO: may be obosolete @ThatOneCalculator
|
||||
meta: IEndpointMeta;
|
||||
params: Schema;
|
||||
}
|
||||
|
||||
const endpoints: IEndpoint[] = eps.map(([name, ep]) => {
|
||||
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
|
||||
return {
|
||||
name: name,
|
||||
exec: ep.default,
|
||||
meta: ep.meta || {},
|
||||
meta: ep.meta ?? {},
|
||||
params: ep.paramDef,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ export default define(meta, paramDef, async (ps, _me) => {
|
|||
const noUsers =
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
})) === 0;
|
||||
if (!(noUsers || me?.isAdmin)) throw new Error("access denied");
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import define from "../../define.js";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { User } from "@/models/entities/user.js";
|
||||
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
||||
import { publishInternalEvent } from "@/services/stream.js";
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
||||
|
@ -29,17 +29,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
throw new Error("user is not local user");
|
||||
}
|
||||
|
||||
/*if (user.isAdmin) {
|
||||
throw new Error('cannot suspend admin');
|
||||
}
|
||||
if (user.isModerator) {
|
||||
throw new Error('cannot suspend moderator');
|
||||
}*/
|
||||
|
||||
await Users.update(user.id, {
|
||||
driveCapacityOverrideMb: ps.overrideMb,
|
||||
});
|
||||
|
||||
publishInternalEvent("localUserUpdated", {
|
||||
id: user.id,
|
||||
});
|
||||
|
||||
insertModerationLog(me, "change-drive-capacity-override", {
|
||||
targetId: user.id,
|
||||
});
|
||||
|
|
|
@ -37,7 +37,10 @@ export const paramDef = {
|
|||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1, maxLength: 100 },
|
||||
src: { type: "string", enum: ["home", "all", "users", "list", "group"] },
|
||||
src: {
|
||||
type: "string",
|
||||
enum: ["home", "all", "users", "list", "group", "instances"],
|
||||
},
|
||||
userListId: { type: "string", format: "misskey:id", nullable: true },
|
||||
userGroupId: { type: "string", format: "misskey:id", nullable: true },
|
||||
keywords: {
|
||||
|
@ -64,6 +67,12 @@ export const paramDef = {
|
|||
type: "string",
|
||||
},
|
||||
},
|
||||
instances: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
caseSensitive: { type: "boolean" },
|
||||
withReplies: { type: "boolean" },
|
||||
withFile: { type: "boolean" },
|
||||
|
@ -75,6 +84,7 @@ export const paramDef = {
|
|||
"keywords",
|
||||
"excludeKeywords",
|
||||
"users",
|
||||
"instances",
|
||||
"caseSensitive",
|
||||
"withReplies",
|
||||
"withFile",
|
||||
|
@ -118,6 +128,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
keywords: ps.keywords,
|
||||
excludeKeywords: ps.excludeKeywords,
|
||||
users: ps.users,
|
||||
instances: ps.instances,
|
||||
caseSensitive: ps.caseSensitive,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
|
|
|
@ -43,7 +43,10 @@ export const paramDef = {
|
|||
properties: {
|
||||
antennaId: { type: "string", format: "misskey:id" },
|
||||
name: { type: "string", minLength: 1, maxLength: 100 },
|
||||
src: { type: "string", enum: ["home", "all", "users", "list", "group"] },
|
||||
src: {
|
||||
type: "string",
|
||||
enum: ["home", "all", "users", "list", "group", "instances"],
|
||||
},
|
||||
userListId: { type: "string", format: "misskey:id", nullable: true },
|
||||
userGroupId: { type: "string", format: "misskey:id", nullable: true },
|
||||
keywords: {
|
||||
|
@ -70,6 +73,12 @@ export const paramDef = {
|
|||
type: "string",
|
||||
},
|
||||
},
|
||||
instances: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
caseSensitive: { type: "boolean" },
|
||||
withReplies: { type: "boolean" },
|
||||
withFile: { type: "boolean" },
|
||||
|
@ -82,6 +91,7 @@ export const paramDef = {
|
|||
"keywords",
|
||||
"excludeKeywords",
|
||||
"users",
|
||||
"instances",
|
||||
"caseSensitive",
|
||||
"withReplies",
|
||||
"withFile",
|
||||
|
@ -131,6 +141,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
keywords: ps.keywords,
|
||||
excludeKeywords: ps.excludeKeywords,
|
||||
users: ps.users,
|
||||
instances: ps.instances,
|
||||
caseSensitive: ps.caseSensitive,
|
||||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import define from "../../define.js";
|
||||
import { Channels, ChannelFollowings } from "@/models/index.js";
|
||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["channels", "account"],
|
||||
|
@ -33,11 +32,24 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = makePaginationQuery(
|
||||
ChannelFollowings.createQueryBuilder(),
|
||||
ps.sinceId,
|
||||
ps.untilId,
|
||||
).andWhere({ followerId: me.id });
|
||||
const query = ChannelFollowings.createQueryBuilder("following").andWhere({
|
||||
followerId: me.id,
|
||||
});
|
||||
if (ps.sinceId) {
|
||||
query.andWhere('following."followeeId" > :sinceId', {
|
||||
sinceId: ps.sinceId,
|
||||
});
|
||||
}
|
||||
if (ps.untilId) {
|
||||
query.andWhere('following."followeeId" < :untilId', {
|
||||
untilId: ps.untilId,
|
||||
});
|
||||
}
|
||||
if (ps.sinceId && !ps.untilId) {
|
||||
query.orderBy('following."followeeId"', "ASC");
|
||||
} else {
|
||||
query.orderBy('following."followeeId"', "DESC");
|
||||
}
|
||||
|
||||
const followings = await query.take(ps.limit).getMany();
|
||||
|
||||
|
|
|
@ -102,10 +102,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
if (typeof ps.blocked === "boolean") {
|
||||
const meta = await fetchMeta(true);
|
||||
if (ps.blocked) {
|
||||
if (meta.blockedHosts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
query.andWhere("instance.host IN (:...blocks)", {
|
||||
blocks: meta.blockedHosts,
|
||||
});
|
||||
} else {
|
||||
} else if (meta.blockedHosts.length > 0) {
|
||||
query.andWhere("instance.host NOT IN (:...blocks)", {
|
||||
blocks: meta.blockedHosts,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { readdir } from "fs/promises";
|
||||
import define from "../define.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: false,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
const music_files: (string | null)[] = [null];
|
||||
const directory = (
|
||||
await readdir("./assets/sounds", { withFileTypes: true })
|
||||
).filter((potentialFolder) => potentialFolder.isDirectory());
|
||||
for await (const folder of directory) {
|
||||
const files = (await readdir(`./assets/sounds/${folder.name}`)).filter(
|
||||
(potentialSong) => potentialSong.endsWith(".mp3"),
|
||||
);
|
||||
for await (const file of files) {
|
||||
music_files.push(`${folder.name}/${file.replace(".mp3", "")}`);
|
||||
}
|
||||
}
|
||||
return music_files;
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { ApiError } from "../../../error.js";
|
||||
import define from "../../../define.js";
|
||||
import { RegistryItems } from "@/models/index.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: false,
|
||||
|
||||
errors: {
|
||||
noSuchKey: {
|
||||
message: "No such key.",
|
||||
code: "NO_SUCH_KEY",
|
||||
id: "ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: { type: "string" },
|
||||
scope: {
|
||||
type: "array",
|
||||
default: [],
|
||||
items: {
|
||||
type: "string",
|
||||
pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["key"],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
if (ps.key !== "reactions") return;
|
||||
const query = RegistryItems.createQueryBuilder("item")
|
||||
.where("item.domain IS NULL")
|
||||
.andWhere("item.userId = :userId", { userId: user.id })
|
||||
.andWhere("item.key = :key", { key: ps.key })
|
||||
.andWhere("item.scope = :scope", { scope: ps.scope });
|
||||
|
||||
const item = await query.getOne();
|
||||
|
||||
if (item == null) {
|
||||
throw new ApiError(meta.errors.noSuchKey);
|
||||
}
|
||||
|
||||
return item.value;
|
||||
});
|
|
@ -489,6 +489,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
requireSetup:
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
})) === 0,
|
||||
}
|
||||
: {}),
|
||||
|
|
|
@ -27,6 +27,11 @@ export const paramDef = {
|
|||
properties: {
|
||||
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: "integer", default: 0 },
|
||||
origin: {
|
||||
type: "string",
|
||||
enum: ["combined", "local", "remote"],
|
||||
default: "local",
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -37,7 +42,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.addSelect("note.score")
|
||||
.where("note.userHost IS NULL")
|
||||
// .where("note.userHost IS NULL")
|
||||
.andWhere("note.score > 0")
|
||||
.andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) })
|
||||
.andWhere("note.visibility = 'public'")
|
||||
|
@ -53,6 +58,15 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
||||
|
||||
switch (ps.origin) {
|
||||
case "local":
|
||||
query.andWhere("note.userHost IS NULL");
|
||||
break;
|
||||
case "remote":
|
||||
query.andWhere("note.userHost IS NOT NULL");
|
||||
break;
|
||||
}
|
||||
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(ps.tag)) throw new Error("Injection");
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw "Injection";
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
} else {
|
||||
query.andWhere(
|
||||
|
@ -102,7 +102,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
qb.orWhere(
|
||||
new Brackets((qb) => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(tag)) throw new Error("Injection");
|
||||
if (!safeForSql(normalizeForSearch(ps.tag)))
|
||||
throw "Injection";
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -7,6 +7,7 @@ import Router from "@koa/router";
|
|||
import multer from "@koa/multer";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
import cors from "@koa/cors";
|
||||
import { apiMastodonCompatible } from "./mastodon/ApiMastodonCompatibleService.js";
|
||||
import { Instances, AccessTokens, Users } from "@/models/index.js";
|
||||
import config from "@/config/index.js";
|
||||
import endpoints from "./endpoints.js";
|
||||
|
@ -18,6 +19,7 @@ import signupPending from "./private/signup-pending.js";
|
|||
import discord from "./service/discord.js";
|
||||
import github from "./service/github.js";
|
||||
import twitter from "./service/twitter.js";
|
||||
import { koaBody } from "koa-body";
|
||||
|
||||
// Init app
|
||||
const app = new Koa();
|
||||
|
@ -34,16 +36,10 @@ app.use(async (ctx, next) => {
|
|||
await next();
|
||||
});
|
||||
|
||||
app.use(
|
||||
bodyParser({
|
||||
// リクエストが multipart/form-data でない限りはJSONだと見なす
|
||||
detectJSON: (ctx) =>
|
||||
!(
|
||||
ctx.is("multipart/form-data") ||
|
||||
ctx.is("application/x-www-form-urlencoded")
|
||||
),
|
||||
}),
|
||||
);
|
||||
// Init router
|
||||
const router = new Router();
|
||||
const mastoRouter = new Router();
|
||||
const errorRouter = new Router();
|
||||
|
||||
// Init multer instance
|
||||
const upload = multer({
|
||||
|
@ -54,8 +50,36 @@ const upload = multer({
|
|||
},
|
||||
});
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
router.use(
|
||||
bodyParser({
|
||||
// リクエストが multipart/form-data でない限りはJSONだと見なす
|
||||
detectJSON: (ctx) =>
|
||||
!(
|
||||
ctx.is("multipart/form-data") ||
|
||||
ctx.is("application/x-www-form-urlencoded")
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
apiMastodonCompatible(mastoRouter);
|
||||
|
||||
/**
|
||||
* Register endpoint handlers
|
||||
|
@ -141,11 +165,13 @@ router.post("/miauth/:session/check", async (ctx) => {
|
|||
});
|
||||
|
||||
// Return 404 for unknown API
|
||||
router.all("(.*)", async (ctx) => {
|
||||
errorRouter.all("(.*)", async (ctx) => {
|
||||
ctx.status = 404;
|
||||
});
|
||||
|
||||
// Register router
|
||||
app.use(mastoRouter.routes());
|
||||
app.use(router.routes());
|
||||
app.use(errorRouter.routes());
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import { apiAuthMastodon } from "./endpoints/auth.js";
|
||||
import { apiAccountMastodon } from "./endpoints/account.js";
|
||||
import { apiStatusMastodon } from "./endpoints/status.js";
|
||||
import { apiFilterMastodon } from "./endpoints/filter.js";
|
||||
import { apiTimelineMastodon } from "./endpoints/timeline.js";
|
||||
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
|
||||
import { apiSearchMastodon } from "./endpoints/search.js";
|
||||
import { getInstance } from "./endpoints/meta.js";
|
||||
|
||||
export function getClient(
|
||||
BASE_URL: string,
|
||||
authorization: string | undefined,
|
||||
): MegalodonInterface {
|
||||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(
|
||||
"misskey",
|
||||
BASE_URL,
|
||||
accessToken,
|
||||
) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
export function apiMastodonCompatible(router: Router): void {
|
||||
apiAuthMastodon(router);
|
||||
apiAccountMastodon(router);
|
||||
apiStatusMastodon(router);
|
||||
apiFilterMastodon(router);
|
||||
apiTimelineMastodon(router);
|
||||
apiNotificationsMastodon(router);
|
||||
apiSearchMastodon(router);
|
||||
|
||||
router.get("/v1/custom_emojis", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceCustomEmojis();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/instance", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
ctx.body = await getInstance(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
import { Users } from "@/models/index.js";
|
||||
import { resolveUser } from "@/remote/resolve-user.js";
|
||||
import Router from "@koa/router";
|
||||
import { FindOptionsWhere, IsNull } from "typeorm";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { argsToBools, limitToInt } from "./timeline.js";
|
||||
|
||||
const relationshopModel = {
|
||||
id: "",
|
||||
following: false,
|
||||
followed_by: false,
|
||||
delivery_following: false,
|
||||
blocking: false,
|
||||
blocked_by: false,
|
||||
muting: false,
|
||||
muting_notifications: false,
|
||||
requested: false,
|
||||
domain_blocking: false,
|
||||
showing_reblogs: false,
|
||||
endorsed: false,
|
||||
notifying: false,
|
||||
note: "",
|
||||
};
|
||||
|
||||
export function apiAccountMastodon(router: Router): void {
|
||||
router.get("/v1/accounts/verify_credentials", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.verifyAccountCredentials();
|
||||
const acct = data.data;
|
||||
acct.url = `${BASE_URL}/@${acct.url}`;
|
||||
acct.note = "";
|
||||
acct.avatar_static = acct.avatar;
|
||||
acct.header = acct.header || "";
|
||||
acct.header_static = acct.header || "";
|
||||
acct.source = {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
language: "",
|
||||
};
|
||||
ctx.body = acct;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.patch("/v1/accounts/update_credentials", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.updateCredentials(
|
||||
(ctx.request as any).body as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/accounts/lookup", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
let userArray = ctx.query.acct?.toString().split("@");
|
||||
let userid;
|
||||
if (userArray === undefined) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { error: "no user specified" };
|
||||
return;
|
||||
}
|
||||
if (userArray.length === 1) {
|
||||
const q: FindOptionsWhere<User> = {
|
||||
usernameLower: userArray[0].toLowerCase(),
|
||||
host: IsNull(),
|
||||
};
|
||||
|
||||
const user = await Users.findOneBy(q);
|
||||
userid = user?.id;
|
||||
} else {
|
||||
userid = (await resolveUser(userArray[0], userArray[1])).id;
|
||||
}
|
||||
const data = await client.getAccount(userid ? userid : "");
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id(^.*\\d.*$)",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccount(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/statuses",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountStatuses(
|
||||
ctx.params.id,
|
||||
argsToBools(limitToInt(ctx.query as any)),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/followers",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountFollowers(
|
||||
ctx.params.id,
|
||||
ctx.query as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/following",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountFollowing(
|
||||
ctx.params.id,
|
||||
ctx.query as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/lists",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountLists(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/follow",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.followAccount(ctx.params.id);
|
||||
const acct = data.data;
|
||||
acct.following = true;
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unfollow",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unfollowAccount(ctx.params.id);
|
||||
const acct = data.data;
|
||||
acct.following = false;
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/block",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.blockAccount(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unblock",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unblockAccount(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/mute",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.muteAccount(
|
||||
ctx.params.id,
|
||||
(ctx.request as any).body as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unmute",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unmuteAccount(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/accounts/relationships", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
let users;
|
||||
try {
|
||||
// TODO: this should be body
|
||||
const idsRaw = ctx.request.query ? ctx.request.query["id[]"] : null;
|
||||
const ids = typeof idsRaw === "string" ? [idsRaw] : idsRaw;
|
||||
users = ids;
|
||||
relationshopModel.id = idsRaw?.toString() || "1";
|
||||
if (!idsRaw) {
|
||||
ctx.body = [relationshopModel];
|
||||
return;
|
||||
}
|
||||
const data = await client.getRelationships(ids);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
let data = e.response.data;
|
||||
data.users = users;
|
||||
console.error(data);
|
||||
ctx.status = 401;
|
||||
ctx.body = data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/bookmarks", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = (await client.getBookmarks(ctx.query as any)) as any;
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/favourites", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getFavourites(ctx.query as any);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/mutes", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getMutes(ctx.query as any);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/blocks", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getBlocks(ctx.query as any);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/follow_ctxs", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getFollowRequests(
|
||||
((ctx.query as any) || { limit: 20 }).limit,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/follow_ctxs/:id/authorize",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.acceptFollowRequest(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/follow_ctxs/:id/reject",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.rejectFollowRequest(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { koaBody } from "koa-body";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
|
||||
const readScope = [
|
||||
"read:account",
|
||||
"read:drive",
|
||||
"read:blocks",
|
||||
"read:favorites",
|
||||
"read:following",
|
||||
"read:messaging",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:reactions",
|
||||
"read:pages",
|
||||
"read:page-likes",
|
||||
"read:user-groups",
|
||||
"read:channels",
|
||||
"read:gallery",
|
||||
"read:gallery-likes",
|
||||
];
|
||||
const writeScope = [
|
||||
"write:account",
|
||||
"write:drive",
|
||||
"write:blocks",
|
||||
"write:favorites",
|
||||
"write:following",
|
||||
"write:messaging",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"write:notifications",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
"write:pages",
|
||||
"write:page-likes",
|
||||
"write:user-groups",
|
||||
"write:channels",
|
||||
"write:gallery",
|
||||
"write:gallery-likes",
|
||||
];
|
||||
|
||||
export function apiAuthMastodon(router: Router): void {
|
||||
router.post("/v1/apps", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
console.log(body);
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
||||
}
|
||||
const scopeArr = Array.from(pushScope);
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await client.registerApp(body.client_name, {
|
||||
scopes: scopeArr,
|
||||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
ctx.body = {
|
||||
id: appData.id,
|
||||
name: appData.name,
|
||||
website: appData.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
|
||||
export function apiFilterMastodon(router: Router): void {
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilter(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.createFilter(body.phrase, body.context, body);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.updateFilter(
|
||||
ctx.params.id,
|
||||
body.phrase,
|
||||
body.context,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.deleteFilter(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import { Entity } from "@calckey/megalodon";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
|
||||
// TODO: add calckey features
|
||||
export async function getInstance(response: Entity.Instance) {
|
||||
const meta = await fetchMeta(true);
|
||||
return {
|
||||
uri: response.uri,
|
||||
title: response.title || "",
|
||||
short_description: response.description || "",
|
||||
description: response.description || "",
|
||||
email: response.email || "",
|
||||
version: "3.0.0 compatible (Calckey)",
|
||||
urls: response.urls,
|
||||
stats: response.stats,
|
||||
thumbnail: response.thumbnail || "",
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
invites_enabled: response.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: 3000,
|
||||
max_media_attachments: 4,
|
||||
characters_reserved_per_url: response.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg",
|
||||
"audio/wave",
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/x-pn-wave",
|
||||
"audio/vnd.wave",
|
||||
"audio/ogg",
|
||||
"audio/vorbis",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/flac",
|
||||
"audio/aac",
|
||||
"audio/m4a",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"audio/3gpp",
|
||||
"video/x-ms-asf",
|
||||
],
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 8,
|
||||
max_characters_per_option: 50,
|
||||
min_expiration: 300,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
},
|
||||
contact_account: {
|
||||
id: "1",
|
||||
username: "admin",
|
||||
acct: "admin",
|
||||
display_name: "admin",
|
||||
locked: true,
|
||||
bot: true,
|
||||
discoverable: false,
|
||||
group: false,
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
note: "Please refer to the original instance for the actual admin contact.",
|
||||
url: "/",
|
||||
avatar: "/static-assets/badges/info.png",
|
||||
avatar_static: "/static-assets/badges/info.png",
|
||||
header: "https://http.cat/404",
|
||||
header_static: "https://http.cat/404",
|
||||
followers_count: -1,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
last_status_at: Math.floor(new Date().getTime() / 1000),
|
||||
noindex: true,
|
||||
emojis: [],
|
||||
fields: [],
|
||||
},
|
||||
rules: [],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { koaBody } from "koa-body";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { toTextWithReaction } from "./timeline.js";
|
||||
function toLimitToInt(q: any) {
|
||||
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
||||
return q;
|
||||
}
|
||||
|
||||
export function apiNotificationsMastodon(router: Router): void {
|
||||
router.get("/v1/notifications", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getNotifications(toLimitToInt(ctx.query));
|
||||
const notfs = data.data;
|
||||
const ret = notfs.map((n) => {
|
||||
if (n.type !== "follow" && n.type !== "follow_request") {
|
||||
if (n.type === "reaction") n.type = "favourite";
|
||||
n.status = toTextWithReaction(
|
||||
n.status ? [n.status] : [],
|
||||
ctx.hostname,
|
||||
)[0];
|
||||
return n;
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
});
|
||||
ctx.body = ret;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/notification/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const dataRaw = await client.getNotification(ctx.params.id);
|
||||
const data = dataRaw.data;
|
||||
if (data.type !== "follow" && data.type !== "follow_request") {
|
||||
if (data.type === "reaction") data.type = "favourite";
|
||||
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
|
||||
} else {
|
||||
ctx.body = data;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/notifications/clear", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.dismissNotifications();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/notification/:id/dismiss", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.dismissNotification(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
|
||||
export function apiSearchMastodon(router: Router): void {
|
||||
router.get("/v1/search", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
const type = query.type || "";
|
||||
const data = await client.search(query.q, type, query);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,483 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import fs from "fs";
|
||||
import { pipeline } from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
import { createTemp } from "@/misc/create-temp.js";
|
||||
import { emojiRegex, emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
||||
import axios from "axios";
|
||||
const pump = promisify(pipeline);
|
||||
|
||||
export function apiStatusMastodon(router: Router): void {
|
||||
router.post("/v1/statuses", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const body: any = ctx.request.body;
|
||||
const text = body.status;
|
||||
const removed = text.replace(/@\S+/g, "").replaceAll(" ", "");
|
||||
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
|
||||
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
|
||||
if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
|
||||
const a = await client.createEmojiReaction(
|
||||
body.in_reply_to_id,
|
||||
removed,
|
||||
);
|
||||
ctx.body = a.data;
|
||||
}
|
||||
if (body.in_reply_to_id && removed === "/unreact") {
|
||||
try {
|
||||
const id = body.in_reply_to_id;
|
||||
const post = await client.getStatus(id);
|
||||
const react = post.data.emoji_reactions.filter((e) => e.me)[0].name;
|
||||
const data = await client.deleteEmojiReaction(id, react);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
}
|
||||
if (!body.media_ids) body.media_ids = undefined;
|
||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||
const data = await client.postStatus(text, body);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
interface IReaction {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
user: MisskeyEntity.User;
|
||||
type: string;
|
||||
}
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/context",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const id = ctx.params.id;
|
||||
const data = await client.getStatusContext(id, ctx.query as any);
|
||||
const status = await client.getStatus(id);
|
||||
const reactionsAxios = await axios.get(
|
||||
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
||||
);
|
||||
const reactions: IReaction[] = reactionsAxios.data;
|
||||
const text = reactions
|
||||
.map((r) => `${r.type.replace("@.", "")} ${r.user.username}`)
|
||||
.join("<br />");
|
||||
data.data.descendants.unshift(
|
||||
statusModel(
|
||||
status.data.id,
|
||||
status.data.account.id,
|
||||
status.data.emojis,
|
||||
text,
|
||||
),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/reblogged_by",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getStatusRebloggedBy(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/favourited_by",
|
||||
async (ctx, reply) => {
|
||||
ctx.body = [];
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/favourite",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const react = await getFirstReaction(BASE_URL, accessTokens);
|
||||
try {
|
||||
const a = (await client.createEmojiReaction(
|
||||
ctx.params.id,
|
||||
react,
|
||||
)) as any;
|
||||
//const data = await client.favouriteStatus(ctx.params.id) as any;
|
||||
ctx.body = a.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unfavourite",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const react = await getFirstReaction(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteEmojiReaction(ctx.params.id, react);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/reblog",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.reblogStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unreblog",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unreblogStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/bookmark",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.bookmarkStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unbookmark",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = (await client.unbookmarkStatus(ctx.params.id)) as any;
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/pin",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.pinStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unpin",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.unpinStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post("/v1/media", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
return;
|
||||
}
|
||||
const [path] = await createTemp();
|
||||
await pump(multipartData.buffer, fs.createWriteStream(path));
|
||||
const image = fs.readFileSync(path);
|
||||
const data = await client.uploadMedia(image);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.post("/v2/media", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
return;
|
||||
}
|
||||
const [path] = await createTemp();
|
||||
await pump(multipartData.buffer, fs.createWriteStream(path));
|
||||
const image = fs.readFileSync(path);
|
||||
const data = await client.uploadMedia(image);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/media/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getMedia(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.put<{ Params: { id: string } }>(
|
||||
"/v1/media/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.updateMedia(
|
||||
ctx.params.id,
|
||||
ctx.request.body as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/polls/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getPoll(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/polls/:id/votes",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.votePoll(
|
||||
ctx.params.id,
|
||||
(ctx.request.body as any).choices,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function getFirstReaction(
|
||||
BASE_URL: string,
|
||||
accessTokens: string | undefined,
|
||||
) {
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
let react = "⭐";
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/i/registry/get-unsecure`, {
|
||||
scope: ["client", "base"],
|
||||
key: "reactions",
|
||||
i: accessToken,
|
||||
});
|
||||
const reactRaw = api.data;
|
||||
react = Array.isArray(reactRaw) ? api.data[0] : "⭐";
|
||||
console.log(api.data);
|
||||
return react;
|
||||
} catch (e) {
|
||||
return react;
|
||||
}
|
||||
}
|
||||
|
||||
export function statusModel(
|
||||
id: string | null,
|
||||
acctId: string | null,
|
||||
emojis: MastodonEntity.Emoji[],
|
||||
content: string,
|
||||
) {
|
||||
const now = Math.floor(new Date().getTime() / 1000);
|
||||
return {
|
||||
id: "9atm5frjhb",
|
||||
uri: "https://http.cat/404", // ""
|
||||
url: "https://http.cat/404", // "",
|
||||
account: {
|
||||
id: "9arzuvv0sw",
|
||||
username: "Reactions",
|
||||
acct: "Reactions",
|
||||
display_name: "Reactions to this post",
|
||||
locked: false,
|
||||
created_at: now,
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
note: "",
|
||||
url: "https://http.cat/404",
|
||||
avatar: "/static-assets/badges/info.png",
|
||||
avatar_static: "/static-assets/badges/info.png",
|
||||
header: "https://http.cat/404", // ""
|
||||
header_static: "https://http.cat/404", // ""
|
||||
emojis: [],
|
||||
fields: [],
|
||||
moved: null,
|
||||
bot: false,
|
||||
},
|
||||
in_reply_to_id: id,
|
||||
in_reply_to_account_id: acctId,
|
||||
reblog: null,
|
||||
content: `<p>${content}</p>`,
|
||||
plain_content: null,
|
||||
created_at: now,
|
||||
emojis: emojis,
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
muted: false,
|
||||
sensitive: false,
|
||||
spoiler_text: "",
|
||||
visibility: "public" as const,
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
tags: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
application: null,
|
||||
language: null,
|
||||
pinned: false,
|
||||
emoji_reactions: [],
|
||||
bookmarked: false,
|
||||
quote: false,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { Entity, MegalodonInterface } from "@calckey/megalodon";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { statusModel } from "./status.js";
|
||||
import Autolinker from "autolinker";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
|
||||
export function limitToInt(q: ParsedUrlQuery) {
|
||||
let object: any = q;
|
||||
if (q.limit)
|
||||
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
|
||||
return q;
|
||||
}
|
||||
|
||||
export function argsToBools(q: ParsedUrlQuery) {
|
||||
let object: any = q;
|
||||
if (q.only_media)
|
||||
if (typeof q.only_media === "string")
|
||||
object.only_media = q.only_media.toLowerCase() === "true";
|
||||
if (q.exclude_replies)
|
||||
if (typeof q.exclude_replies === "string")
|
||||
object.exclude_replies = q.exclude_replies.toLowerCase() === "true";
|
||||
return q;
|
||||
}
|
||||
|
||||
export function toTextWithReaction(status: Entity.Status[], host: string) {
|
||||
return status.map((t) => {
|
||||
if (!t) return statusModel(null, null, [], "no content");
|
||||
if (!t.emoji_reactions) return t;
|
||||
if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0];
|
||||
const reactions = t.emoji_reactions.map(
|
||||
(r) => `${r.name.replace("@.", "")} (${r.count}${r.me ? "* " : ""})`,
|
||||
);
|
||||
//t.emojis = getEmoji(t.content, host)
|
||||
t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(
|
||||
", ",
|
||||
)}</p>`;
|
||||
return t;
|
||||
});
|
||||
}
|
||||
export function autoLinker(input: string, host: string) {
|
||||
return Autolinker.link(input, {
|
||||
hashtag: "twitter",
|
||||
mention: "twitter",
|
||||
email: false,
|
||||
stripPrefix: false,
|
||||
replaceFn: function (match) {
|
||||
switch (match.type) {
|
||||
case "url":
|
||||
return true;
|
||||
case "mention":
|
||||
console.log("Mention: ", match.getMention());
|
||||
console.log("Mention Service Name: ", match.getServiceName());
|
||||
return `<a href="https://${host}/@${encodeURIComponent(
|
||||
match.getMention(),
|
||||
)}" target="_blank">@${match.getMention()}</a>`;
|
||||
case "hashtag":
|
||||
console.log("Hashtag: ", match.getHashtag());
|
||||
return `<a href="https://${host}/tags/${encodeURIComponent(
|
||||
match.getHashtag(),
|
||||
)}" target="_blank">#${match.getHashtag()}</a>`;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function apiTimelineMastodon(router: Router): void {
|
||||
router.get("/v1/timelines/public", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
const data = query.local
|
||||
? await client.getLocalTimeline(limitToInt(query))
|
||||
: await client.getPublicTimeline(limitToInt(query));
|
||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
"/v1/timelines/tag/:hashtag",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getTagTimeline(
|
||||
ctx.params.hashtag,
|
||||
limitToInt(ctx.query),
|
||||
);
|
||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
"/v1/timelines/home",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getHomeTimeline(limitToInt(ctx.query));
|
||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { listId: string } }>(
|
||||
"/v1/timelines/list/:listId",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getListTimeline(
|
||||
ctx.params.listId,
|
||||
limitToInt(ctx.query),
|
||||
);
|
||||
ctx.body = toTextWithReaction(data.data, ctx.hostname);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/conversations", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getConversationTimeline(limitToInt(ctx.query));
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/lists", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getLists();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getList(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post("/v1/lists", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.createList((ctx.query as any).title);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.updateList(ctx.params.id, ctx.query as any);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteList(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountsInList(
|
||||
ctx.params.id,
|
||||
ctx.query as any,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.addAccountsToList(
|
||||
ctx.params.id,
|
||||
(ctx.query as any).account_ids,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteAccountsFromList(
|
||||
ctx.params.id,
|
||||
(ctx.query as any).account_ids,
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
function escapeHTML(str: string) {
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
function nl2br(str: string) {
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
str = str.replace(/\r\n/g, "<br />");
|
||||
str = str.replace(/(\n|\r)/g, "<br />");
|
||||
return str;
|
||||
}
|
|
@ -24,6 +24,9 @@ import { readNotification } from "../common/read-notification.js";
|
|||
import channels from "./channels/index.js";
|
||||
import type Channel from "./channel.js";
|
||||
import type { StreamEventEmitter, StreamMessages } from "./types.js";
|
||||
import { Converter } from "@calckey/megalodon";
|
||||
import { getClient } from "../mastodon/ApiMastodonCompatibleService.js";
|
||||
import { toTextWithReaction } from "../mastodon/endpoints/timeline.js";
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
|
@ -41,17 +44,27 @@ export default class Connection {
|
|||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
private cachedNotes: Packed<"Note">[] = [];
|
||||
private isMastodonCompatible: boolean = false;
|
||||
private host: string;
|
||||
private accessToken: string;
|
||||
private currentSubscribe: string[][] = [];
|
||||
|
||||
constructor(
|
||||
wsConnection: websocket.connection,
|
||||
subscriber: EventEmitter,
|
||||
user: User | null | undefined,
|
||||
token: AccessToken | null | undefined,
|
||||
host: string,
|
||||
accessToken: string,
|
||||
prepareStream: string | undefined,
|
||||
) {
|
||||
console.log("constructor", prepareStream);
|
||||
this.wsConnection = wsConnection;
|
||||
this.subscriber = subscriber;
|
||||
if (user) this.user = user;
|
||||
if (token) this.token = token;
|
||||
if (host) this.host = host;
|
||||
if (accessToken) this.accessToken = accessToken;
|
||||
|
||||
this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this);
|
||||
this.onUserEvent = this.onUserEvent.bind(this);
|
||||
|
@ -73,6 +86,13 @@ export default class Connection {
|
|||
|
||||
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
|
||||
}
|
||||
console.log("prepare", prepareStream);
|
||||
if (prepareStream) {
|
||||
this.onWsConnectionMessage({
|
||||
type: "utf8",
|
||||
utf8Data: JSON.stringify({ stream: prepareStream, type: "subscribe" }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onUserEvent(data: StreamMessages["user"]["payload"]) {
|
||||
|
@ -125,58 +145,149 @@ export default class Connection {
|
|||
if (data.type !== "utf8") return;
|
||||
if (data.utf8Data == null) return;
|
||||
|
||||
let obj: Record<string, any>;
|
||||
let objs: Record<string, any>[];
|
||||
|
||||
try {
|
||||
obj = JSON.parse(data.utf8Data);
|
||||
objs = [JSON.parse(data.utf8Data)];
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = obj;
|
||||
const simpleObj = objs[0];
|
||||
if (simpleObj.stream) {
|
||||
// is Mastodon Compatible
|
||||
this.isMastodonCompatible = true;
|
||||
if (simpleObj.type === "subscribe") {
|
||||
let forSubscribe = [];
|
||||
if (simpleObj.stream === "user") {
|
||||
this.currentSubscribe.push(["user"]);
|
||||
objs = [
|
||||
{
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "main",
|
||||
id: simpleObj.stream,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "homeTimeline",
|
||||
id: simpleObj.stream,
|
||||
},
|
||||
},
|
||||
];
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
try {
|
||||
const tl = await client.getHomeTimeline();
|
||||
for (const t of tl.data) forSubscribe.push(t.id);
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.error(e.response.data);
|
||||
}
|
||||
} else if (simpleObj.stream === "public:local") {
|
||||
this.currentSubscribe.push(["public:local"]);
|
||||
objs = [
|
||||
{
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "localTimeline",
|
||||
id: simpleObj.stream,
|
||||
},
|
||||
},
|
||||
];
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
const tl = await client.getLocalTimeline();
|
||||
for (const t of tl.data) forSubscribe.push(t.id);
|
||||
} else if (simpleObj.stream === "public") {
|
||||
this.currentSubscribe.push(["public"]);
|
||||
objs = [
|
||||
{
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "globalTimeline",
|
||||
id: simpleObj.stream,
|
||||
},
|
||||
},
|
||||
];
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
const tl = await client.getPublicTimeline();
|
||||
for (const t of tl.data) forSubscribe.push(t.id);
|
||||
} else if (simpleObj.stream === "list") {
|
||||
this.currentSubscribe.push(["list", simpleObj.list]);
|
||||
objs = [
|
||||
{
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "list",
|
||||
id: simpleObj.stream,
|
||||
params: {
|
||||
listId: simpleObj.list,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
const tl = await client.getListTimeline(simpleObj.list);
|
||||
for (const t of tl.data) forSubscribe.push(t.id);
|
||||
}
|
||||
for (const s of forSubscribe) {
|
||||
objs.push({
|
||||
type: "s",
|
||||
body: {
|
||||
id: s,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "readNotification":
|
||||
this.onReadNotification(body);
|
||||
break;
|
||||
case "subNote":
|
||||
this.onSubscribeNote(body);
|
||||
break;
|
||||
case "s":
|
||||
this.onSubscribeNote(body);
|
||||
break; // alias
|
||||
case "sr":
|
||||
this.onSubscribeNote(body);
|
||||
this.readNote(body);
|
||||
break;
|
||||
case "unsubNote":
|
||||
this.onUnsubscribeNote(body);
|
||||
break;
|
||||
case "un":
|
||||
this.onUnsubscribeNote(body);
|
||||
break; // alias
|
||||
case "connect":
|
||||
this.onChannelConnectRequested(body);
|
||||
break;
|
||||
case "disconnect":
|
||||
this.onChannelDisconnectRequested(body);
|
||||
break;
|
||||
case "channel":
|
||||
this.onChannelMessageRequested(body);
|
||||
break;
|
||||
case "ch":
|
||||
this.onChannelMessageRequested(body);
|
||||
break; // alias
|
||||
for (const obj of objs) {
|
||||
const { type, body } = obj;
|
||||
console.log(type, body);
|
||||
switch (type) {
|
||||
case "readNotification":
|
||||
this.onReadNotification(body);
|
||||
break;
|
||||
case "subNote":
|
||||
this.onSubscribeNote(body);
|
||||
break;
|
||||
case "s":
|
||||
this.onSubscribeNote(body);
|
||||
break; // alias
|
||||
case "sr":
|
||||
this.onSubscribeNote(body);
|
||||
this.readNote(body);
|
||||
break;
|
||||
case "unsubNote":
|
||||
this.onUnsubscribeNote(body);
|
||||
break;
|
||||
case "un":
|
||||
this.onUnsubscribeNote(body);
|
||||
break; // alias
|
||||
case "connect":
|
||||
this.onChannelConnectRequested(body);
|
||||
break;
|
||||
case "disconnect":
|
||||
this.onChannelDisconnectRequested(body);
|
||||
break;
|
||||
case "channel":
|
||||
this.onChannelMessageRequested(body);
|
||||
break;
|
||||
case "ch":
|
||||
this.onChannelMessageRequested(body);
|
||||
break; // alias
|
||||
|
||||
// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
|
||||
// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
|
||||
// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
|
||||
case "typingOnChannel":
|
||||
this.typingOnChannel(body.channel);
|
||||
break;
|
||||
case "typingOnMessaging":
|
||||
this.typingOnMessaging(body);
|
||||
break;
|
||||
// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
|
||||
// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
|
||||
// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
|
||||
case "typingOnChannel":
|
||||
this.typingOnChannel(body.channel);
|
||||
break;
|
||||
case "typingOnMessaging":
|
||||
this.typingOnMessaging(body);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,12 +391,75 @@ export default class Connection {
|
|||
* クライアントにメッセージ送信
|
||||
*/
|
||||
public sendMessageToWs(type: string, payload: any) {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
type: type,
|
||||
body: payload,
|
||||
}),
|
||||
);
|
||||
console.log(payload, this.isMastodonCompatible);
|
||||
if (this.isMastodonCompatible) {
|
||||
if (payload.type === "note") {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream: [payload.id],
|
||||
event: "update",
|
||||
payload: JSON.stringify(
|
||||
toTextWithReaction(
|
||||
[Converter.note(payload.body, this.host)],
|
||||
this.host,
|
||||
)[0],
|
||||
),
|
||||
}),
|
||||
);
|
||||
this.onSubscribeNote({
|
||||
id: payload.body.id,
|
||||
});
|
||||
} else if (payload.type === "reacted" || payload.type === "unreacted") {
|
||||
// reaction
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
client.getStatus(payload.id).then((data) => {
|
||||
const newPost = toTextWithReaction([data.data], this.host);
|
||||
for (const stream of this.currentSubscribe) {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream,
|
||||
event: "status.update",
|
||||
payload: JSON.stringify(newPost[0]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (payload.type === "deleted") {
|
||||
// delete
|
||||
for (const stream of this.currentSubscribe) {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream,
|
||||
event: "delete",
|
||||
payload: payload.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else if (payload.type === "unreadNotification") {
|
||||
if (payload.id === "user") {
|
||||
const body = Converter.notification(payload.body, this.host);
|
||||
if (body.type === "reaction") body.type = "favourite";
|
||||
body.status = toTextWithReaction(
|
||||
body.status ? [body.status] : [],
|
||||
"",
|
||||
)[0];
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream: ["user"],
|
||||
event: "notification",
|
||||
payload: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
type: type,
|
||||
body: payload,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,6 @@ import type { Note } from "@/models/entities/note.js";
|
|||
import type { Antenna } from "@/models/entities/antenna.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type { DriveFolder } from "@/models/entities/drive-folder.js";
|
||||
import { Emoji } from "@/models/entities/emoji.js";
|
||||
import type { UserList } from "@/models/entities/user-list.js";
|
||||
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||
import type { UserGroup } from "@/models/entities/user-group.js";
|
||||
|
@ -23,7 +22,10 @@ export interface InternalStreamTypes {
|
|||
id: User["id"];
|
||||
isSuspended: User["isSuspended"];
|
||||
};
|
||||
userChangeSilencedState: { id: User["id"]; isSilenced: User["isSilenced"] };
|
||||
userChangeSilencedState: {
|
||||
id: User["id"];
|
||||
isSilenced: User["isSilenced"];
|
||||
};
|
||||
userChangeModeratorState: {
|
||||
id: User["id"];
|
||||
isModerator: User["isModerator"];
|
||||
|
@ -33,7 +35,12 @@ export interface InternalStreamTypes {
|
|||
oldToken: User["token"];
|
||||
newToken: User["token"];
|
||||
};
|
||||
remoteUserUpdated: { id: User["id"] };
|
||||
localUserUpdated: {
|
||||
id: User["id"];
|
||||
};
|
||||
remoteUserUpdated: {
|
||||
id: User["id"];
|
||||
};
|
||||
webhookCreated: Webhook;
|
||||
webhookDeleted: Webhook;
|
||||
webhookUpdated: Webhook;
|
||||
|
@ -135,6 +142,9 @@ export interface NoteStreamTypes {
|
|||
reaction: string;
|
||||
userId: User["id"];
|
||||
};
|
||||
replied: {
|
||||
id: Note["id"];
|
||||
};
|
||||
}
|
||||
type NoteStreamEventTypes = {
|
||||
[key in keyof NoteStreamTypes]: {
|
||||
|
|
|
@ -16,10 +16,13 @@ export const initializeStreamingServer = (server: http.Server) => {
|
|||
|
||||
ws.on("request", async (request) => {
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
const headers = request.httpRequest.headers["sec-websocket-protocol"] || "";
|
||||
const cred = q.i || q.access_token || headers;
|
||||
const accessToken = cred.toString();
|
||||
|
||||
const [user, app] = await authenticate(
|
||||
request.httpRequest.headers.authorization,
|
||||
q.i,
|
||||
accessToken,
|
||||
).catch((err) => {
|
||||
request.reject(403, err.message);
|
||||
return [];
|
||||
|
@ -43,8 +46,19 @@ export const initializeStreamingServer = (server: http.Server) => {
|
|||
}
|
||||
|
||||
redisClient.on("message", onRedisMessage);
|
||||
const host = `https://${request.host}`;
|
||||
const prepareStream = q.stream?.toString();
|
||||
console.log("start", q);
|
||||
|
||||
const main = new MainStreamConnection(connection, ev, user, app);
|
||||
const main = new MainStreamConnection(
|
||||
connection,
|
||||
ev,
|
||||
user,
|
||||
app,
|
||||
host,
|
||||
accessToken,
|
||||
prepareStream,
|
||||
);
|
||||
|
||||
const intervalId = user
|
||||
? setInterval(() => {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { createTemp } from "@/misc/create-temp.js";
|
|||
import { publishMainStream } from "@/services/stream.js";
|
||||
import * as Acct from "@/misc/acct.js";
|
||||
import { envOption } from "@/env.js";
|
||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import activityPub from "./activitypub.js";
|
||||
import nodeinfo from "./nodeinfo.js";
|
||||
import wellKnown from "./well-known.js";
|
||||
|
@ -28,6 +29,7 @@ import fileServer from "./file/index.js";
|
|||
import proxyServer from "./proxy/index.js";
|
||||
import webServer from "./web/index.js";
|
||||
import { initializeStreamingServer } from "./api/streaming.js";
|
||||
import { koaBody } from "koa-body";
|
||||
|
||||
export const serverLogger = new Logger("server", "gray", false);
|
||||
|
||||
|
@ -68,6 +70,24 @@ app.use(mount("/proxy", proxyServer));
|
|||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
const mastoRouter = new Router();
|
||||
|
||||
mastoRouter.use(
|
||||
koaBody({
|
||||
urlencoded: true,
|
||||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// Routing
|
||||
router.use(activityPub.routes());
|
||||
|
@ -133,7 +153,52 @@ router.get("/verify-email/:code", async (ctx) => {
|
|||
}
|
||||
});
|
||||
|
||||
mastoRouter.get("/oauth/authorize", async (ctx) => {
|
||||
const client_id = ctx.request.query.client_id;
|
||||
console.log(ctx.request.req);
|
||||
ctx.redirect(Buffer.from(client_id?.toString() || "", "base64").toString());
|
||||
});
|
||||
|
||||
mastoRouter.post("/oauth/token", async (ctx) => {
|
||||
const body: any = ctx.request.body;
|
||||
let client_id: any = ctx.request.query.client_id;
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator("misskey", BASE_URL, null) as MegalodonInterface;
|
||||
let m = null;
|
||||
if (body.code) {
|
||||
m = body.code.match(/^[a-zA-Z0-9-]+/);
|
||||
if (!m.length) {
|
||||
ctx.body = { error: "Invalid code" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (client_id instanceof Array) {
|
||||
client_id = client_id.toString();
|
||||
} else if (!client_id) {
|
||||
client_id = null;
|
||||
}
|
||||
try {
|
||||
const atData = await client.fetchAccessToken(
|
||||
client_id,
|
||||
body.client_secret,
|
||||
m ? m[0] : "",
|
||||
);
|
||||
ctx.body = {
|
||||
access_token: atData.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: "read write follow",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ctx.status = 401;
|
||||
ctx.body = err.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
// Register router
|
||||
app.use(mastoRouter.routes());
|
||||
app.use(router.routes());
|
||||
|
||||
app.use(mount(webServer));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
window.onload = async () => {
|
||||
const account = JSON.parse(localStorage.getItem('account'));
|
||||
const account = JSON.parse(localStorage.getItem("account"));
|
||||
const i = account.token;
|
||||
|
||||
const api = (endpoint, data = {}) => {
|
||||
|
@ -10,42 +10,44 @@ window.onload = async () => {
|
|||
if (i) data.i = i;
|
||||
|
||||
// Send request
|
||||
fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
|
||||
method: 'POST',
|
||||
fetch(endpoint.indexOf("://") > -1 ? endpoint : `/api/${endpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache'
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
credentials: "omit",
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
}).catch(reject);
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
const content = document.getElementById('content');
|
||||
const content = document.getElementById("content");
|
||||
|
||||
document.getElementById('ls').addEventListener('click', () => {
|
||||
content.innerHTML = '';
|
||||
document.getElementById("ls").addEventListener("click", () => {
|
||||
content.innerHTML = "";
|
||||
|
||||
const lsEditor = document.createElement('div');
|
||||
lsEditor.id = 'lsEditor';
|
||||
const lsEditor = document.createElement("div");
|
||||
lsEditor.id = "lsEditor";
|
||||
|
||||
const adder = document.createElement('div');
|
||||
adder.classList.add('adder');
|
||||
const addKeyInput = document.createElement('input');
|
||||
const addValueTextarea = document.createElement('textarea');
|
||||
const addButton = document.createElement('button');
|
||||
addButton.textContent = 'Add';
|
||||
addButton.addEventListener('click', () => {
|
||||
const adder = document.createElement("div");
|
||||
adder.classList.add("adder");
|
||||
const addKeyInput = document.createElement("input");
|
||||
const addValueTextarea = document.createElement("textarea");
|
||||
const addButton = document.createElement("button");
|
||||
addButton.textContent = "Add";
|
||||
addButton.addEventListener("click", () => {
|
||||
localStorage.setItem(addKeyInput.value, addValueTextarea.value);
|
||||
location.reload();
|
||||
});
|
||||
|
@ -57,21 +59,21 @@ window.onload = async () => {
|
|||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i);
|
||||
const record = document.createElement('div');
|
||||
record.classList.add('record');
|
||||
const header = document.createElement('header');
|
||||
const record = document.createElement("div");
|
||||
record.classList.add("record");
|
||||
const header = document.createElement("header");
|
||||
header.textContent = k;
|
||||
const textarea = document.createElement('textarea');
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.textContent = localStorage.getItem(k);
|
||||
const saveButton = document.createElement('button');
|
||||
saveButton.textContent = 'Save';
|
||||
saveButton.addEventListener('click', () => {
|
||||
const saveButton = document.createElement("button");
|
||||
saveButton.textContent = "Save";
|
||||
saveButton.addEventListener("click", () => {
|
||||
localStorage.setItem(k, textarea.value);
|
||||
location.reload();
|
||||
});
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.textContent = 'Remove';
|
||||
removeButton.addEventListener('click', () => {
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.textContent = "Remove";
|
||||
removeButton.addEventListener("click", () => {
|
||||
localStorage.removeItem(k);
|
||||
location.reload();
|
||||
});
|
||||
|
|
|
@ -9,120 +9,122 @@
|
|||
* 注: webpackは介さないため、このファイルではrequireやimportは使えません。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
||||
(async () => {
|
||||
window.onerror = (e) => {
|
||||
console.error(e);
|
||||
renderError('SOMETHING_HAPPENED', e);
|
||||
renderError("SOMETHING_HAPPENED", e);
|
||||
};
|
||||
window.onunhandledrejection = (e) => {
|
||||
console.error(e);
|
||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||
renderError("SOMETHING_HAPPENED_IN_PROMISE", e);
|
||||
};
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
const v = localStorage.getItem('v') || VERSION;
|
||||
const v = localStorage.getItem("v") || VERSION;
|
||||
|
||||
const supportedLangs = LANGS;
|
||||
let lang = localStorage.getItem('lang');
|
||||
let lang = localStorage.getItem("lang");
|
||||
if (lang == null || !supportedLangs.includes(lang)) {
|
||||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
lang = supportedLangs.find((x) => x.split("-")[0] === navigator.language);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'en-US';
|
||||
if (lang == null) lang = "en-US";
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`/assets/locales/${lang}.${v}.json`);
|
||||
if (res.status === 200) {
|
||||
localStorage.setItem('lang', lang);
|
||||
localStorage.setItem('locale', await res.text());
|
||||
localStorage.setItem('localeVersion', v);
|
||||
localStorage.setItem("lang", lang);
|
||||
localStorage.setItem("locale", await res.text());
|
||||
localStorage.setItem("localeVersion", v);
|
||||
} else {
|
||||
await checkUpdate();
|
||||
renderError('LOCALE_FETCH');
|
||||
renderError("LOCALE_FETCH");
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Script
|
||||
function importAppScript() {
|
||||
import(`/assets/${CLIENT_ENTRY}`)
|
||||
.catch(async e => {
|
||||
await checkUpdate();
|
||||
console.error(e);
|
||||
renderError('APP_IMPORT', e);
|
||||
});
|
||||
import(`/assets/${CLIENT_ENTRY}`).catch(async (e) => {
|
||||
await checkUpdate();
|
||||
console.error(e);
|
||||
renderError("APP_IMPORT", e);
|
||||
});
|
||||
}
|
||||
|
||||
// タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
|
||||
if (document.readyState !== 'loading') {
|
||||
if (document.readyState !== "loading") {
|
||||
importAppScript();
|
||||
} else {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
importAppScript();
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Theme
|
||||
const theme = localStorage.getItem('theme');
|
||||
const theme = localStorage.getItem("theme");
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
|
||||
// HTMLの theme-color 適用
|
||||
if (k === 'htmlThemeColor') {
|
||||
if (k === "htmlThemeColor") {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
if (
|
||||
tag.tagName === "META" &&
|
||||
tag.getAttribute("name") === "theme-color"
|
||||
) {
|
||||
tag.setAttribute("content", v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const colorSchema = localStorage.getItem('colorSchema');
|
||||
const colorSchema = localStorage.getItem("colorSchema");
|
||||
if (colorSchema) {
|
||||
document.documentElement.style.setProperty('color-schema', colorSchema);
|
||||
document.documentElement.style.setProperty("color-schema", colorSchema);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const fontSize = localStorage.getItem('fontSize');
|
||||
const fontSize = localStorage.getItem("fontSize");
|
||||
if (fontSize) {
|
||||
document.documentElement.classList.add('f-' + fontSize);
|
||||
document.documentElement.classList.add("f-" + fontSize);
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem('useSystemFont');
|
||||
const useSystemFont = localStorage.getItem("useSystemFont");
|
||||
if (useSystemFont) {
|
||||
document.documentElement.classList.add('useSystemFont');
|
||||
document.documentElement.classList.add("useSystemFont");
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper');
|
||||
const wallpaper = localStorage.getItem("wallpaper");
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
|
||||
}
|
||||
|
||||
const customCss = localStorage.getItem('customCss');
|
||||
const customCss = localStorage.getItem("customCss");
|
||||
if (customCss && customCss.length > 0) {
|
||||
const style = document.createElement('style');
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = customCss;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async function addStyle(styleText) {
|
||||
let css = document.createElement('style');
|
||||
let css = document.createElement("style");
|
||||
css.appendChild(document.createTextNode(styleText));
|
||||
document.head.appendChild(css);
|
||||
}
|
||||
|
||||
function renderError(code, details) {
|
||||
let errorsElement = document.getElementById('errors');
|
||||
let errorsElement = document.getElementById("errors");
|
||||
|
||||
if (!errorsElement) {
|
||||
document.body.innerHTML = `
|
||||
|
@ -158,9 +160,9 @@
|
|||
<br>
|
||||
<div id="errors"></div>
|
||||
`;
|
||||
errorsElement = document.getElementById('errors');
|
||||
errorsElement = document.getElementById("errors");
|
||||
}
|
||||
const detailsElement = document.createElement('details');
|
||||
const detailsElement = document.createElement("details");
|
||||
detailsElement.innerHTML = `
|
||||
<br>
|
||||
<summary>
|
||||
|
@ -278,25 +280,25 @@
|
|||
details {
|
||||
width: 50%;
|
||||
}
|
||||
`)
|
||||
`);
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
try {
|
||||
const res = await fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
cache: 'no-cache'
|
||||
const res = await fetch("/api/meta", {
|
||||
method: "POST",
|
||||
cache: "no-cache",
|
||||
});
|
||||
|
||||
const meta = await res.json();
|
||||
|
||||
if (meta.version != v) {
|
||||
localStorage.setItem('v', meta.version);
|
||||
localStorage.setItem("v", meta.version);
|
||||
refresh();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
renderError('UPDATE_CHECK', e);
|
||||
renderError("UPDATE_CHECK", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -304,9 +306,9 @@
|
|||
function refresh() {
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => registration.unregister());
|
||||
navigator.serviceWorker.controller.postMessage("clear");
|
||||
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
||||
registrations.forEach((registration) => registration.unregister());
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -1,51 +1,53 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
window.onload = async () => {
|
||||
const account = JSON.parse(localStorage.getItem('account'));
|
||||
const account = JSON.parse(localStorage.getItem("account"));
|
||||
const i = account.token;
|
||||
|
||||
const api = (endpoint, data = {}) => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
// Append a credential
|
||||
if (i) data.i = i;
|
||||
|
||||
|
||||
// Send request
|
||||
fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
|
||||
method: 'POST',
|
||||
fetch(endpoint.indexOf("://") > -1 ? endpoint : `/api/${endpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache'
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
}).catch(reject);
|
||||
credentials: "omit",
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
document.getElementById('submit').addEventListener('click', () => {
|
||||
api('notes/create', {
|
||||
text: document.getElementById('text').value
|
||||
document.getElementById("submit").addEventListener("click", () => {
|
||||
api("notes/create", {
|
||||
text: document.getElementById("text").value,
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
api('notes/timeline').then(notes => {
|
||||
const tl = document.getElementById('tl');
|
||||
api("notes/timeline").then((notes) => {
|
||||
const tl = document.getElementById("tl");
|
||||
for (const note of notes) {
|
||||
const el = document.createElement('div');
|
||||
const name = document.createElement('header');
|
||||
const el = document.createElement("div");
|
||||
const name = document.createElement("header");
|
||||
name.textContent = `${note.user.name} @${note.user.username}`;
|
||||
const text = document.createElement('div');
|
||||
const text = document.createElement("div");
|
||||
text.textContent = `${note.text}`;
|
||||
el.appendChild(name);
|
||||
el.appendChild(text);
|
||||
|
|
|
@ -634,6 +634,10 @@ router.get("/streaming", async (ctx) => {
|
|||
ctx.status = 503;
|
||||
ctx.set("Cache-Control", "private, max-age=0");
|
||||
});
|
||||
router.get("/api/v1/streaming", async (ctx) => {
|
||||
ctx.status = 503;
|
||||
ctx.set("Cache-Control", "private, max-age=0");
|
||||
});
|
||||
|
||||
// Render base html for all requests
|
||||
router.get("(.*)", async (ctx) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
html {
|
||||
html, body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,23 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => {
|
|||
|
||||
logger.succ(`Got preview of ${url}: ${summary.title}`);
|
||||
|
||||
if (
|
||||
summary.url &&
|
||||
!(summary.url.startsWith("http://") || summary.url.startsWith("https://"))
|
||||
) {
|
||||
throw new Error("unsupported schema included");
|
||||
}
|
||||
|
||||
if (
|
||||
summary.player?.url &&
|
||||
!(
|
||||
summary.player.url.startsWith("http://") ||
|
||||
summary.player.url.startsWith("https://")
|
||||
)
|
||||
) {
|
||||
throw new Error("unsupported schema included");
|
||||
}
|
||||
|
||||
summary.icon = wrap(summary.icon);
|
||||
summary.thumbnail = wrap(summary.thumbnail);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ block vars
|
|||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const isImage = note.files.length !== 0 && note.files[0].type.startsWith('image');
|
||||
- const isVideo = note.files.length !== 0 && note.files[0].type.startsWith('video');
|
||||
- const imageUrl = isImage ? note.files[0].url : isVideo ? note.files[0].thumbnailUrl : avatarUrl;
|
||||
- const imageUrl = isImage ? note.files[0].url : isVideo ? note.files[0].thumbnailUrl : avatarUrl;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
@ -23,7 +23,7 @@ block og
|
|||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= imageUrl)
|
||||
if isImage
|
||||
if isImage && !note.files[0].isSensitive
|
||||
meta(property='og:image:width' content=note.files[0].properties.width)
|
||||
meta(property='og:image:height' content=note.files[0].properties.height)
|
||||
meta(property='og:image:type' content=note.files[0].type)
|
||||
|
|
|
@ -209,12 +209,12 @@ export default async function (
|
|||
await Blockings.delete(blocking.id);
|
||||
} else {
|
||||
// それ以外は単純に例外
|
||||
if (blocking != null)
|
||||
if (blocking)
|
||||
throw new IdentifiableError(
|
||||
"710e8fb0-b8c3-4922-be49-d5d93d8e6a6e",
|
||||
"blocking",
|
||||
);
|
||||
if (blocked != null)
|
||||
if (blocked)
|
||||
throw new IdentifiableError(
|
||||
"3338392a-f764-498d-8855-db939dcf8c48",
|
||||
"blocked",
|
||||
|
|
|
@ -38,8 +38,8 @@ export default async function (
|
|||
}),
|
||||
]);
|
||||
|
||||
if (blocking != null) throw new Error("blocking");
|
||||
if (blocked != null) throw new Error("blocked");
|
||||
if (blocking) throw new Error("blocking");
|
||||
if (blocked) throw new Error("blocked");
|
||||
|
||||
const followRequest = await FollowRequests.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import * as mfm from "mfm-js";
|
||||
import es from "../../db/elasticsearch.js";
|
||||
import { publishMainStream, publishNotesStream } from "@/services/stream.js";
|
||||
import {
|
||||
publishMainStream,
|
||||
publishNotesStream,
|
||||
publishNoteStream,
|
||||
} from "@/services/stream.js";
|
||||
import DeliverManager from "@/remote/activitypub/deliver-manager.js";
|
||||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||
import renderCreate from "@/remote/activitypub/renderer/create.js";
|
||||
|
@ -430,6 +434,12 @@ export default async (
|
|||
}
|
||||
|
||||
publishNotesStream(note);
|
||||
if (note.replyId != null) {
|
||||
// Only provide the reply note id here as the recipient may not be authorized to see the note.
|
||||
publishNoteStream(note.replyId, "replied", {
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
const webhooks = await getActiveWebhooks().then((webhooks) =>
|
||||
webhooks.filter((x) => x.userId === user.id && x.on.includes("note")),
|
||||
|
|
|
@ -21,6 +21,16 @@ subscriber.on("message", async (_, data) => {
|
|||
if (obj.channel === "internal") {
|
||||
const { type, body } = obj.message;
|
||||
switch (type) {
|
||||
case "localUserUpdated": {
|
||||
userByIdCache.delete(body.id);
|
||||
localUserByIdCache.delete(body.id);
|
||||
localUserByNativeTokenCache.cache.forEach((v, k) => {
|
||||
if (v.value?.id === body.id) {
|
||||
localUserByNativeTokenCache.delete(k);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "userChangeSuspendedState":
|
||||
case "userChangeSilencedState":
|
||||
case "userChangeModeratorState":
|
||||
|
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 68 KiB |
|
@ -6,12 +6,24 @@
|
|||
"build": "pnpm vite build",
|
||||
"lint": "pnpm rome check \"src/**/*.{ts,vue}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@khmyznikov/pwa-install": "^0.2.0",
|
||||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/seedrandom": "3.0.4",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"autobind-decorator": "2.4.0",
|
||||
|
@ -19,16 +31,21 @@
|
|||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.19.1",
|
||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git",
|
||||
"calckey-js": "^0.0.20",
|
||||
"calckey-js": "^0.0.22",
|
||||
"chart.js": "4.1.1",
|
||||
"chartjs-adapter-date-fns": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.5.1",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"chartjs-chart-matrix": "^2.0.1",
|
||||
"city-timezones": "^1.2.1",
|
||||
"compare-versions": "5.0.3",
|
||||
"cropperjs": "2.0.0-beta.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"date-fns": "2.29.3",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"gsap": "^3.11.4",
|
||||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.3",
|
||||
|
@ -40,9 +57,11 @@
|
|||
"punycode": "2.1.1",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.9.1",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.57.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"swiper": "^8.4.5",
|
||||
|
@ -57,28 +76,11 @@
|
|||
"typescript": "4.9.4",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "^4.1.0-beta.1",
|
||||
"vite": "^4.1.1",
|
||||
"vue": "3.2.45",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
"vue-plyr": "^7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/seedrandom": "3.0.4",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"rollup": "3.9.1",
|
||||
"start-server-and-test": "1.15.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button class="nrvgflfu _button" @click="toggle">
|
||||
<button class="nrvgflfu _button" @click.stop.prevent="toggle">
|
||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||
<span v-if="!modelValue">{{ label }}</span>
|
||||
</button>
|
||||
|
|
|
@ -1,51 +1,51 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<div class="mk-dialog">
|
||||
<div v-if="icon" class="icon">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div v-else-if="!input && !select" class="icon" :class="type">
|
||||
<i v-if="type === 'success'" class="ph-check-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'error'" class="ph-circle-wavy-warning-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'warning'" class="ph-warning-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'info'" class="ph-info-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'question'" class="ph-question-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'waiting'" class="ph-circle-notch-bold ph-lg fa-pulse"></i>
|
||||
</div>
|
||||
<header v-if="title"><Mfm :text="title"/></header>
|
||||
<div v-if="text" class="body"><Mfm :text="text"/></div>
|
||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ph-lock-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
<template v-if="select.items">
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
|
||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
|
||||
<div v-if="!isYesNo">
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div v-if="icon" :class="$style.icon">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
|
||||
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
|
||||
<i v-if="type === 'success'" :class="$style.iconInner" class="ph-check-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-circle-wavy-warning-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info-bold ph-lg"></i>
|
||||
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-circle-question-bold ph-lg"></i>
|
||||
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
|
||||
</div>
|
||||
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
|
||||
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
|
||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ph-password-bold ph-lg"></i></template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" autofocus>
|
||||
<template v-if="select.items">
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
|
||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
</MkSelect>
|
||||
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
|
||||
<div v-if="!isYesNo">
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ i18n.ts.yes }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.no }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); modal?.close(); }">{{ action.text }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="actions" class="buttons">
|
||||
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
|
@ -88,12 +88,16 @@ const props = withDefaults(defineProps<{
|
|||
showOkButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
isYesNo?: boolean;
|
||||
|
||||
cancelableByBgClick?: boolean;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
}>(), {
|
||||
type: 'info',
|
||||
showOkButton: true,
|
||||
showCancelButton: false,
|
||||
isYesNo: false,
|
||||
|
||||
cancelableByBgClick: true,
|
||||
});
|
||||
|
||||
|
@ -102,7 +106,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof MkModal>>();
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const inputValue = ref(props.input?.default || null);
|
||||
const selectedValue = ref(props.select?.default || null);
|
||||
|
@ -151,9 +155,10 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-dialog {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
|
@ -161,56 +166,56 @@ onBeforeUnmount(() => {
|
|||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
font-size: 32px;
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
|
||||
&.info {
|
||||
color: #55c4dd;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
& + header {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> header {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
|
||||
& + .body {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
margin-top: 16px;
|
||||
|
||||
> * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
& + .title {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.iconInner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.type_info {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.type_success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.type_error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.type_warning {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
|
||||
& + .text {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|