This commit is contained in:
usbharu 2024-06-01 18:23:44 +09:00
parent 671d1e4f9e
commit 869ef1e111
210 changed files with 1055 additions and 11855 deletions

View File

@ -0,0 +1,21 @@
plugins {
kotlin("jvm") version "1.9.23"
}
group = "dev.usbharu"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Sat Jun 01 11:47:11 JST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
hideout-activitypub/gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
hideout-activitypub/gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
rootProject.name = "hideout-activitypub"

View File

@ -0,0 +1,5 @@
package dev.usbharu
fun main() {
println("Hello World!")
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import java.net.MalformedURLException

View File

@ -17,8 +17,6 @@
package mastodon.account
import dev.usbharu.hideout.SpringApplication
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.infrastructure.exposedquery.FollowerQueryServiceImpl
import dev.usbharu.owl.producer.api.OwlProducer
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest

View File

@ -18,7 +18,6 @@ package util
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain
import dev.usbharu.hideout.activitypub.domain.model.StringOrObject
object Constant {
val context = listOf(
StringOrObject("https://www.w3.org/ns/activitystreams"),
StringOrObject("https://w3id.org/security/v1"),
StringOrObject(
mapOf(
"manuallyApprovesFollowers" to "as:manuallyApprovesFollowers",
"sensitive" to "as:sensitive",
"Hashtag" to "as:Hashtag",
"quoteUrl" to "as:quoteUrl",
"toot" to "http://joinmastodon.org/ns#",
"Emoji" to "toot:Emoji",
"featured" to "toot:featured",
"discoverable" to "toot:discoverable",
"schema" to "http://schema.org#",
"PropertyValue" to "schema:PropertyValue",
"value" to "schema:value",
)
)
)
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class ActivityPubProcessException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = 5370068873167636639L
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class FailedProcessException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = -1305337651143409144L
}
}

View File

@ -1,32 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import java.io.Serial
class FailedToGetActivityPubResourceException : FailedToGetResourcesException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 6420233106776818052L
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class HttpSignatureUnauthorizedException : RuntimeException {
constructor() : super()
constructor(message: String?) : super(message)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(
message,
cause,
enableSuppression,
writableStackTrace
)
companion object {
@Serial
private const val serialVersionUID: Long = -6449793151674654501L
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class IllegalActivityPubObjectException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 7216998115771415263L
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.exception
import java.io.Serial
class JsonParseException : IllegalArgumentException {
constructor() : super()
constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object {
@Serial
private const val serialVersionUID: Long = 7975567796830950692L
}
}

View File

@ -1,62 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Accept @JsonCreator constructor(
type: List<String> = emptyList(),
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object")
val apObject: Object,
override val actor: String
) : Object(
type = add(type, "Accept")
),
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Accept
if (apObject != other.apObject) return false
if (actor != other.actor) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
return result
}
override fun toString(): String {
return "Accept(" +
"apObject=$apObject, " +
"actor='$actor'" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,76 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Announce @JsonCreator constructor(
type: List<String> = emptyList(),
@JsonProperty("object")
val apObject: String,
override val actor: String,
override val id: String,
val published: String,
val to: List<String> = emptyList(),
val cc: List<String> = emptyList()
) : Object(
type = add(type, "Announce")
),
HasActor,
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Announce
if (apObject != other.apObject) return false
if (actor != other.actor) return false
if (id != other.id) return false
if (published != other.published) return false
if (to != other.to) return false
if (cc != other.cc) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
return result
}
override fun toString(): String {
return "Announce(" +
"apObject='$apObject', " +
"actor='$actor', " +
"id='$id', " +
"published='$published', " +
"to=$to, " +
"cc=$cc" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Create(
type: List<String> = emptyList(),
val name: String? = null,
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object")
val apObject: Object,
override val actor: String,
override val id: String,
val to: List<String> = emptyList(),
val cc: List<String> = emptyList()
) : Object(
type = add(type, "Create")
),
HasId,
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Create
if (name != other.name) return false
if (apObject != other.apObject) return false
if (actor != other.actor) return false
if (id != other.id) return false
if (to != other.to) return false
if (cc != other.cc) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
return result
}
override fun toString(): String {
return "Create(" +
"name=$name, " +
"apObject=$apObject, " +
"actor='$actor', " +
"id='$id', " +
"to=$to, " +
"cc=$cc" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Delete : Object, HasId, HasActor {
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object")
val apObject: Object
val published: String
override var actor: String = ""
override var id: String = ""
constructor(
type: List<String> = emptyList(),
actor: String,
id: String,
`object`: Object,
published: String
) : super(add(type, "Delete")) {
this.apObject = `object`
this.published = published
this.actor = actor
this.id = id
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Delete
if (apObject != other.apObject) return false
if (published != other.published) return false
if (actor != other.actor) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun toString(): String {
return "Delete(" +
"apObject=$apObject, " +
"published='$published', " +
"actor='$actor', " +
"id='$id'" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,57 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Document(
type: List<String> = emptyList(),
@JsonSetter(nulls = Nulls.AS_EMPTY)
override val name: String = "",
val mediaType: String,
val url: String
) : Object(
type = add(type, "Document")
),
HasName {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Document
if (mediaType != other.mediaType) return false
if (url != other.url) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + mediaType.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + name.hashCode()
return result
}
override fun toString(): String = "Document(mediaType=$mediaType, url=$url, name='$name') ${super.toString()}"
}

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Emoji(
type: List<String>,
override val name: String,
override val id: String,
val updated: String,
val icon: Image
) : Object(
type = add(type, "Emoji")
),
HasName,
HasId {
override fun toString(): String {
return "Emoji(" +
"name='$name', " +
"id='$id', " +
"updated='$updated', " +
"icon=$icon" +
")" +
" ${super.toString()}"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Emoji
if (name != other.name) return false
if (id != other.id) return false
if (updated != other.updated) return false
if (icon != other.icon) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + updated.hashCode()
result = 31 * result + icon.hashCode()
return result
}
}

View File

@ -1,61 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Follow(
type: List<String> = emptyList(),
@JsonProperty("object") val apObject: String,
override val actor: String,
val id: String? = null
) : Object(
type = add(type, "Follow")
),
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Follow
if (apObject != other.apObject) return false
if (actor != other.actor) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Follow(" +
"apObject='$apObject', " +
"actor='$actor', " +
"id=$id" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
interface HasActor {
val actor: String
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
interface HasId {
val id: String
}

View File

@ -1,55 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Image(
type: List<String> = emptyList(),
val mediaType: String? = null,
val url: String
) : Object(
add(type, "Image")
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Image
if (mediaType != other.mediaType) return false
if (url != other.url) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (mediaType?.hashCode() ?: 0)
result = 31 * result + url.hashCode()
return result
}
override fun toString(): String {
return "Image(" +
"mediaType=$mediaType, " +
"url='$url'" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,101 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
open class JsonLd {
@JsonProperty("@context")
@JsonDeserialize(contentUsing = StringOrObjectDeserializer::class)
@JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = ContextSerializer::class)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
var context: List<StringOrObject> = emptyList()
set(value) {
field = value.filterNot { it.isEmpty() }
}
@JsonCreator
constructor(context: List<StringOrObject?>?) {
if (context != null) {
this.context = context.filterNotNull().filterNot { it.isEmpty() }
} else {
this.context = emptyList()
}
}
protected constructor()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is JsonLd) return false
return context == other.context
}
override fun hashCode(): Int = context.hashCode()
override fun toString(): String = "JsonLd(context=$context)"
}
class ContextDeserializer : JsonDeserializer<String>() {
override fun deserialize(
p0: com.fasterxml.jackson.core.JsonParser?,
p1: com.fasterxml.jackson.databind.DeserializationContext?
): String {
val readTree: JsonNode = p0?.codec?.readTree(p0) ?: return ""
if (readTree.isValueNode) {
return readTree.textValue()
}
return ""
}
}
class ContextSerializer : JsonSerializer<List<StringOrObject>>() {
@Deprecated("Deprecated in Java")
override fun isEmpty(value: List<StringOrObject>?): Boolean = value.isNullOrEmpty()
override fun isEmpty(provider: SerializerProvider?, value: List<StringOrObject>?): Boolean = value.isNullOrEmpty()
override fun serialize(value: List<StringOrObject>?, gen: JsonGenerator?, serializers: SerializerProvider) {
if (value.isNullOrEmpty()) {
serializers.defaultSerializeNull(gen)
return
}
if (value.size == 1) {
serializers.findValueSerializer(StringOrObject::class.java).serialize(value[0], gen, serializers)
} else {
gen?.writeStartArray()
value.forEach {
serializers.findValueSerializer(StringOrObject::class.java).serialize(it, gen, serializers)
}
gen?.writeEndArray()
}
}
}

View File

@ -1,53 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Key(
override val id: String,
val owner: String,
val publicKeyPem: String
) : Object(
type = add(list = emptyList(), type = "Key")
),
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Key
if (owner != other.owner) return false
if (publicKeyPem != other.publicKeyPem) return false
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + owner.hashCode()
result = 31 * result + publicKeyPem.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun toString(): String = "Key(owner=$owner, publicKeyPem=$publicKeyPem, id='$id') ${super.toString()}"
}

View File

@ -1,73 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Like(
type: List<String> = emptyList(),
override val actor: String,
override val id: String,
@JsonProperty("object") val apObject: String,
val content: String,
@JsonDeserialize(contentUsing = ObjectDeserializer::class) val tag: List<Object> = emptyList()
) : Object(
type = add(type, "Like")
),
HasId,
HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Like
if (actor != other.actor) return false
if (id != other.id) return false
if (apObject != other.apObject) return false
if (content != other.content) return false
if (tag != other.tag) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + tag.hashCode()
return result
}
override fun toString(): String {
return "Like(" +
"actor='$actor', " +
"id='$id', " +
"apObject='$apObject', " +
"content='$content', " +
"tag=$tag" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Note
@Suppress("LongParameterList", "CyclomaticComplexMethod")
constructor(
type: List<String> = emptyList(),
override val id: String,
val attributedTo: String,
val content: String,
val published: String,
val to: List<String> = emptyList(),
val cc: List<String> = emptyList(),
val sensitive: Boolean = false,
val inReplyTo: String? = null,
val attachment: List<Document> = emptyList(),
@JsonDeserialize(contentUsing = ObjectDeserializer::class)
val tag: List<Object> = emptyList(),
val quoteUri: String? = null,
val quoteUrl: String? = null,
@JsonProperty("_misskey_quote")
val misskeyQuote: String? = null
) : Object(
type = add(type, "Note")
),
HasId {
@Suppress("CyclomaticComplexMethod", "CognitiveComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Note
if (id != other.id) return false
if (attributedTo != other.attributedTo) return false
if (content != other.content) return false
if (published != other.published) return false
if (to != other.to) return false
if (cc != other.cc) return false
if (sensitive != other.sensitive) return false
if (inReplyTo != other.inReplyTo) return false
if (attachment != other.attachment) return false
if (tag != other.tag) return false
if (quoteUri != other.quoteUri) return false
if (quoteUrl != other.quoteUrl) return false
if (misskeyQuote != other.misskeyQuote) return false
return true
}
@Suppress("CyclomaticComplexMethod")
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + attributedTo.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + published.hashCode()
result = 31 * result + to.hashCode()
result = 31 * result + cc.hashCode()
result = 31 * result + sensitive.hashCode()
result = 31 * result + (inReplyTo?.hashCode() ?: 0)
result = 31 * result + attachment.hashCode()
result = 31 * result + tag.hashCode()
result = 31 * result + (quoteUri?.hashCode() ?: 0)
result = 31 * result + (quoteUrl?.hashCode() ?: 0)
result = 31 * result + (misskeyQuote?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Note(" +
"id='$id', " +
"attributedTo='$attributedTo', " +
"content='$content', " +
"published='$published', " +
"to=$to, " +
"cc=$cc, " +
"sensitive=$sensitive, " +
"inReplyTo=$inReplyTo, " +
"attachment=$attachment, " +
"tag=$tag, " +
"quoteUri=$quoteUri, " +
"quoteUrl=$quoteUrl, " +
"misskeyQuote=$misskeyQuote" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,102 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Person
@Suppress("LongParameterList")
constructor(
type: List<String> = emptyList(),
val name: String?,
override val id: String,
var preferredUsername: String,
var summary: String?,
var inbox: String,
var outbox: String,
var url: String,
private var icon: Image?,
var publicKey: Key,
var endpoints: Map<String, String> = emptyMap(),
var followers: String?,
var following: String?,
val manuallyApprovesFollowers: Boolean? = false
) : Object(add(type, "Person")), HasId {
@Suppress("CyclomaticComplexMethod", "CognitiveComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Person
if (name != other.name) return false
if (id != other.id) return false
if (preferredUsername != other.preferredUsername) return false
if (summary != other.summary) return false
if (inbox != other.inbox) return false
if (outbox != other.outbox) return false
if (url != other.url) return false
if (icon != other.icon) return false
if (publicKey != other.publicKey) return false
if (endpoints != other.endpoints) return false
if (followers != other.followers) return false
if (following != other.following) return false
if (manuallyApprovesFollowers != other.manuallyApprovesFollowers) return false
return true
}
@Suppress("CyclomaticComplexMethod")
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + id.hashCode()
result = 31 * result + preferredUsername.hashCode()
result = 31 * result + (summary?.hashCode() ?: 0)
result = 31 * result + inbox.hashCode()
result = 31 * result + outbox.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + (icon?.hashCode() ?: 0)
result = 31 * result + publicKey.hashCode()
result = 31 * result + endpoints.hashCode()
result = 31 * result + (followers?.hashCode() ?: 0)
result = 31 * result + (following?.hashCode() ?: 0)
result = 31 * result + (manuallyApprovesFollowers?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Person(" +
"name=$name, " +
"id='$id', " +
"preferredUsername='$preferredUsername', " +
"summary=$summary, " +
"inbox='$inbox', " +
"outbox='$outbox', " +
"url='$url', " +
"icon=$icon, " +
"publicKey=$publicKey, " +
"endpoints=$endpoints, " +
"followers=$followers, " +
"following=$following, " +
"manuallyApprovesFollowers=$manuallyApprovesFollowers" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,59 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Reject(
override val actor: String,
override val id: String,
@JsonDeserialize(using = ObjectDeserializer::class) @JsonProperty("object") val apObject: Object
) : Object(listOf("Reject")), HasId, HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Reject
if (actor != other.actor) return false
if (id != other.id) return false
if (apObject != other.apObject) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + apObject.hashCode()
return result
}
override fun toString(): String {
return "Reject(" +
"actor='$actor', " +
"id='$id', " +
"apObject=$apObject" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,76 +0,0 @@
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.*
open class StringOrObject {
var contextString: String? = null
var contextObject: Map<String, String>? = null
@JsonCreator
protected constructor()
constructor(string: String) : this() {
contextString = string
}
constructor(contextObject: Map<String, String>) : this() {
this.contextObject = contextObject
}
fun isEmpty(): Boolean = contextString.isNullOrEmpty() and contextObject.isNullOrEmpty()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StringOrObject
if (contextString != other.contextString) return false
if (contextObject != other.contextObject) return false
return true
}
override fun hashCode(): Int {
var result = contextString?.hashCode() ?: 0
result = 31 * result + (contextObject?.hashCode() ?: 0)
return result
}
override fun toString(): String = "StringOrObject(contextString=$contextString, contextObject=$contextObject)"
}
class StringOrObjectDeserializer : JsonDeserializer<StringOrObject>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext): StringOrObject {
val readTree: JsonNode = p?.codec?.readTree(p) ?: return StringOrObject("")
return if (readTree.isValueNode) {
StringOrObject(readTree.textValue())
} else if (readTree.isObject) {
val map: Map<String, String> = ctxt.readTreeAsValue(
readTree,
ctxt.typeFactory.constructType(object : TypeReference<Map<String, String>>() {})
)
StringOrObject(map)
} else {
StringOrObject("")
}
}
}
class StringORObjectSerializer : JsonSerializer<StringOrObject>() {
override fun serialize(value: StringOrObject?, gen: JsonGenerator?, serializers: SerializerProvider) {
if (value == null) {
serializers.defaultSerializeNull(gen)
return
}
if (value.contextString != null) {
gen?.writeString(value.contextString)
} else {
serializers.defaultSerializeValue(value.contextObject, gen)
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
open class Tombstone(type: List<String> = emptyList(), override val id: String) :
Object(add(type, "Tombstone")),
HasId {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Tombstone
return id == other.id
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + id.hashCode()
return result
}
override fun toString(): String = "Tombstone(id='$id') ${super.toString()}"
}

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectDeserializer
open class Undo(
type: List<String> = emptyList(),
override val actor: String,
override val id: String,
@JsonDeserialize(using = ObjectDeserializer::class)
@JsonProperty("object") val apObject: Object,
val published: String?
) : Object(add(type, "Undo")), HasId, HasActor {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Undo
if (actor != other.actor) return false
if (id != other.id) return false
if (apObject != other.apObject) return false
if (published != other.published) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode()
result = 31 * result + apObject.hashCode()
result = 31 * result + (published?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Undo(" +
"actor='$actor', " +
"id='$id', " +
"apObject=$apObject, " +
"published=$published" +
")" +
" ${super.toString()}"
}
}

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model.nodeinfo
data class Nodeinfo(
val links: List<Links>
) {
data class Links(
val rel: String,
val href: String
)
}

View File

@ -1,67 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("ClassName")
package dev.usbharu.hideout.activitypub.domain.model.nodeinfo
@Suppress("ClassNaming")
data class Nodeinfo2_0(
val version: String,
val software: Software,
val protocols: List<String>,
val services: Services,
val openRegistrations: Boolean,
val usage: Usage,
val metadata: Metadata
) {
data class Software(
val name: String,
val version: String
)
data class Services(
val inbound: List<String>,
val outbound: List<String>
)
data class Usage(
val users: Users,
val localPosts: Int,
val localComments: Int
) {
data class Users(
val total: Int,
val activeHalfYear: Int,
val activeMonth: Int
)
}
data class Metadata(
val nodeName: String,
val nodeDescription: String,
val maintainer: Maintainer,
val langs: List<String>,
val tosUrl: String,
val repositoryUrl: String,
val feedbackUrl: String,
) {
data class Maintainer(
val name: String,
val email: String
)
}
}

View File

@ -1,77 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model.objects
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import dev.usbharu.hideout.activitypub.domain.model.JsonLd
open class Object : JsonLd {
@JsonSerialize(using = TypeSerializer::class)
var type: List<String> = emptyList()
set(value) {
field = value.filter { it.isNotBlank() }
}
protected constructor()
constructor(type: List<String>) : super() {
this.type = type.filter { it.isNotBlank() }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as Object
return type == other.type
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + type.hashCode()
return result
}
override fun toString(): String = "Object(type=$type) ${super.toString()}"
companion object {
@JvmStatic
protected fun add(list: List<String>, type: String): List<String> {
val toMutableList = list.toMutableList()
toMutableList.add(type)
return toMutableList.distinct()
}
}
}
class TypeSerializer : JsonSerializer<List<String>>() {
override fun serialize(value: List<String>?, gen: JsonGenerator?, serializers: SerializerProvider?) {
if (value?.size == 1) {
gen?.writeString(value[0])
} else {
gen?.writeStartArray()
value?.forEach {
gen?.writeString(it)
}
gen?.writeEndArray()
}
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model.objects
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.activitypub.domain.model.*
import dev.usbharu.hideout.activitypub.service.common.ExtendedActivityVocabulary
class ObjectDeserializer : JsonDeserializer<Object>() {
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Object? {
requireNotNull(p)
val treeNode: JsonNode = requireNotNull(p.codec?.readTree(p))
if (treeNode.isValueNode) {
return ObjectValue(
emptyList(),
treeNode.asText()
)
} else if (treeNode.isObject) {
val type = treeNode["type"]
val activityType = if (type.isArray) {
type.firstNotNullOf { jsonNode: JsonNode ->
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(jsonNode.asText(), true) }
}
} else if (type.isValueNode) {
ExtendedActivityVocabulary.values().firstOrNull { it.name.equals(type.asText(), true) }
} else {
null
}
return when (activityType) {
ExtendedActivityVocabulary.Follow -> p.codec.treeToValue(treeNode, Follow::class.java)
ExtendedActivityVocabulary.Note -> p.codec.treeToValue(treeNode, Note::class.java)
ExtendedActivityVocabulary.Object -> p.codec.treeToValue(treeNode, Object::class.java)
ExtendedActivityVocabulary.Link -> null
ExtendedActivityVocabulary.Activity -> null
ExtendedActivityVocabulary.IntransitiveActivity -> null
ExtendedActivityVocabulary.Collection -> null
ExtendedActivityVocabulary.OrderedCollection -> null
ExtendedActivityVocabulary.CollectionPage -> null
ExtendedActivityVocabulary.OrderedCollectionPage -> null
ExtendedActivityVocabulary.Accept -> p.codec.treeToValue(treeNode, Accept::class.java)
ExtendedActivityVocabulary.Add -> null
ExtendedActivityVocabulary.Announce -> p.codec.treeToValue(treeNode, Announce::class.java)
ExtendedActivityVocabulary.Arrive -> null
ExtendedActivityVocabulary.Block -> null
ExtendedActivityVocabulary.Create -> p.codec.treeToValue(treeNode, Create::class.java)
ExtendedActivityVocabulary.Delete -> p.codec.treeToValue(treeNode, Delete::class.java)
ExtendedActivityVocabulary.Dislike -> null
ExtendedActivityVocabulary.Flag -> null
ExtendedActivityVocabulary.Ignore -> null
ExtendedActivityVocabulary.Invite -> null
ExtendedActivityVocabulary.Join -> null
ExtendedActivityVocabulary.Leave -> null
ExtendedActivityVocabulary.Like -> p.codec.treeToValue(treeNode, Like::class.java)
ExtendedActivityVocabulary.Listen -> null
ExtendedActivityVocabulary.Move -> null
ExtendedActivityVocabulary.Offer -> null
ExtendedActivityVocabulary.Question -> null
ExtendedActivityVocabulary.Reject -> p.codec.treeToValue(treeNode, Reject::class.java)
ExtendedActivityVocabulary.Read -> null
ExtendedActivityVocabulary.Remove -> null
ExtendedActivityVocabulary.TentativeReject -> null
ExtendedActivityVocabulary.TentativeAccept -> null
ExtendedActivityVocabulary.Travel -> null
ExtendedActivityVocabulary.Undo -> p.codec.treeToValue(treeNode, Undo::class.java)
ExtendedActivityVocabulary.Update -> null
ExtendedActivityVocabulary.View -> null
ExtendedActivityVocabulary.Application -> null
ExtendedActivityVocabulary.Group -> null
ExtendedActivityVocabulary.Organization -> null
ExtendedActivityVocabulary.Person -> p.codec.treeToValue(treeNode, Person::class.java)
ExtendedActivityVocabulary.Service -> null
ExtendedActivityVocabulary.Article -> null
ExtendedActivityVocabulary.Audio -> null
ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java)
ExtendedActivityVocabulary.Event -> null
ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java)
ExtendedActivityVocabulary.Page -> null
ExtendedActivityVocabulary.Place -> null
ExtendedActivityVocabulary.Profile -> null
ExtendedActivityVocabulary.Relationship -> null
ExtendedActivityVocabulary.Tombstone -> p.codec.treeToValue(treeNode, Tombstone::class.java)
ExtendedActivityVocabulary.Video -> null
ExtendedActivityVocabulary.Mention -> null
ExtendedActivityVocabulary.Emoji -> p.codec.treeToValue(treeNode, Emoji::class.java)
null -> null
}
} else {
return null
}
}
}

View File

@ -1,43 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model.objects
import com.fasterxml.jackson.annotation.JsonCreator
@Suppress("VariableNaming")
open class ObjectValue @JsonCreator constructor(type: List<String>, var `object`: String) : Object(
type
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ObjectValue
return `object` == other.`object`
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + `object`.hashCode()
return result
}
override fun toString(): String = "ObjectValue(`object`='$`object`') ${super.toString()}"
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.domain.model.webfinger
data class WebFinger(val subject: String, val links: List<Link>) {
data class Link(val rel: String, val type: String, val href: String)
}

View File

@ -1,82 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.infrastructure.exposedquery
import dev.usbharu.hideout.activitypub.domain.model.Announce
import dev.usbharu.hideout.activitypub.query.AnnounceQueryService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl
import dev.usbharu.hideout.application.infrastructure.exposed.ResultRowMapper
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Actors
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Posts
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.selectAll
import org.springframework.stereotype.Repository
import java.time.Instant
@Repository
class ExposedAnnounceQueryService(
private val postRepository: PostRepository,
private val postResultRowMapper: ResultRowMapper<Post>
) : AnnounceQueryService {
override suspend fun findById(id: Long): Pair<Announce, Post>? {
return Posts
.leftJoin(Actors)
.selectAll().where { Posts.id eq id }
.singleOrNull()
?.let { (it.toAnnounce() ?: return null) to (postResultRowMapper.map(it)) }
}
override suspend fun findByApId(apId: String): Pair<Announce, Post>? {
return Posts
.leftJoin(Actors)
.selectAll().where { Posts.apId eq apId }
.singleOrNull()
?.let { (it.toAnnounce() ?: return null) to (postResultRowMapper.map(it)) }
}
private suspend fun ResultRow.toAnnounce(): Announce? {
val repostId = this[Posts.repostId] ?: return null
val repost = postRepository.findById(repostId)?.url ?: return null
val (to, cc) = visibility(
Visibility.entries.first { visibility -> visibility.ordinal == this[Posts.visibility] },
this[Actors.followers]
)
return Announce(
type = emptyList(),
id = this[Posts.apId],
apObject = repost,
actor = this[Actors.url],
published = Instant.ofEpochMilli(this[Posts.createdAt]).toString(),
to = to,
cc = cc
)
}
private fun visibility(visibility: Visibility, followers: String?): Pair<List<String>, List<String>> {
return when (visibility) {
Visibility.PUBLIC -> listOf(APNoteServiceImpl.public) to listOf(APNoteServiceImpl.public)
Visibility.UNLISTED -> listOfNotNull(followers) to listOf(APNoteServiceImpl.public)
Visibility.FOLLOWERS -> listOfNotNull(followers) to listOfNotNull(followers)
Visibility.DIRECT -> TODO()
}
}
}

View File

@ -1,129 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.infrastructure.exposedquery
import dev.usbharu.hideout.activitypub.domain.model.Document
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteServiceImpl.Companion.public
import dev.usbharu.hideout.application.infrastructure.exposed.QueryMapper
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.infrastructure.exposedrepository.*
import org.jetbrains.exposed.sql.Query
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.selectAll
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository
import java.time.Instant
@Repository
class NoteQueryServiceImpl(private val postRepository: PostRepository, private val postQueryMapper: QueryMapper<Post>) :
NoteQueryService {
override suspend fun findById(id: Long): Pair<Note, Post>? {
return Posts
.leftJoin(Actors)
.leftJoin(PostsMedia)
.leftJoin(Media)
.selectAll().where { Posts.id eq id }
.let {
(it.toNote() ?: return null) to (
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
}
}
override suspend fun findByApid(apId: String): Pair<Note, Post>? {
return Posts
.leftJoin(Actors)
.leftJoin(PostsMedia)
.leftJoin(Media)
.selectAll().where { Posts.apId eq apId }
.let {
(it.toNote() ?: return null) to (
postQueryMapper.map(it)
.singleOrNull() ?: return null
)
}
}
private suspend fun ResultRow.toNote(mediaList: List<dev.usbharu.hideout.core.domain.model.media.Media>): Note {
val replyId = this[Posts.replyId]
val replyTo = if (replyId != null) {
val url = postRepository.findById(replyId)?.url
if (url == null) {
logger.warn("Failed to get replyId: $replyId")
}
url
} else {
null
}
val repostId = this[Posts.repostId]
val repost = if (repostId != null) {
val url = postRepository.findById(repostId)?.url
if (url == null) {
logger.warn("Failed to get repostId: $repostId")
}
url
} else {
null
}
val visibility1 =
visibility(
Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
this[Actors.followers]
)
return Note(
id = this[Posts.apId],
attributedTo = this[Actors.url],
content = this[Posts.text],
published = Instant.ofEpochMilli(this[Posts.createdAt]).toString(),
to = visibility1.first,
cc = visibility1.second,
inReplyTo = replyTo,
misskeyQuote = repost,
quoteUri = repost,
quoteUrl = repost,
sensitive = this[Posts.sensitive],
attachment = mediaList.map { Document(url = it.url, mediaType = "image/jpeg") }
)
}
private suspend fun Query.toNote(): Note? {
return this.groupBy { it[Posts.id] }
.map { it.value }
.map { it.first().toNote(it.mapNotNull { resultRow -> resultRow.toMediaOrNull() }) }
.singleOrNull()
}
private fun visibility(visibility: Visibility, followers: String?): Pair<List<String>, List<String>> {
return when (visibility) {
Visibility.PUBLIC -> listOf(public) to listOf(public)
Visibility.UNLISTED -> listOfNotNull(followers) to listOf(public)
Visibility.FOLLOWERS -> listOfNotNull(followers) to listOfNotNull(followers)
Visibility.DIRECT -> TODO()
}
}
companion object {
private val logger = LoggerFactory.getLogger(NoteQueryServiceImpl::class.java)
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.actor
import dev.usbharu.hideout.activitypub.domain.model.Person
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
interface UserAPController {
@GetMapping("/users/{username}")
suspend fun userAp(@PathVariable("username") username: String): ResponseEntity<Person>
}

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.actor
import dev.usbharu.hideout.activitypub.domain.Constant
import dev.usbharu.hideout.activitypub.domain.model.Person
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
@RestController
class UserAPControllerImpl(private val apUserService: APUserService) : UserAPController {
override suspend fun userAp(username: String): ResponseEntity<Person> {
val person = try {
apUserService.getPersonByName(username)
} catch (_: UserNotFoundException) {
return ResponseEntity.notFound().build()
}
person.context += Constant.context
return ResponseEntity(person, HttpStatus.OK)
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.hostmeta
import dev.usbharu.hideout.application.config.ApplicationConfig
import org.intellij.lang.annotations.Language
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class HostMetaController(private val applicationConfig: ApplicationConfig) {
val xml = //language=XML
"""<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml"
template="${applicationConfig.url}/.well-known/webfinger?resource={uri}"/>
</XRD>"""
@Language("JSON")
val json = """{
"links": [
{
"rel": "lrdd",
"type": "application/jrd+json",
"template": "${applicationConfig.url}/.well-known/webfinger?resource={uri}"
}
]
}"""
@GetMapping("/.well-known/host-meta", produces = ["application/xml"])
fun hostmeta(): ResponseEntity<String> = ResponseEntity(xml, HttpStatus.OK)
@GetMapping("/.well-known/host-meta.json", produces = ["application/json"])
fun hostmetJson(): ResponseEntity<String> = ResponseEntity(json, HttpStatus.OK)
}

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
@RestController
interface InboxController {
@RequestMapping(
"/inbox",
"/users/{username}/inbox",
produces = [
"application/activity+json",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
],
consumes = ["application/json", "application/*+json"],
method = [RequestMethod.POST]
)
suspend fun inbox(httpServletRequest: HttpServletRequest): ResponseEntity<String>
}

View File

@ -1,133 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
import dev.usbharu.hideout.activitypub.service.common.APService
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureHeaderChecker
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders.WWW_AUTHENTICATE
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
import java.net.URL
@RestController
class InboxControllerImpl(
private val apService: APService,
private val httpSignatureHeaderChecker: HttpSignatureHeaderChecker,
) : InboxController {
@Suppress("TooGenericExceptionCaught")
override suspend fun inbox(
httpServletRequest: HttpServletRequest,
): ResponseEntity<String> {
val headersList = httpServletRequest.headerNames?.toList().orEmpty()
LOGGER.trace("Inbox Headers {}", headersList)
val body = withContext(Dispatchers.IO + MDCContext()) {
httpServletRequest.inputStream.readAllBytes()!!
}
val responseEntity = checkHeader(httpServletRequest, body)
if (responseEntity != null) {
return responseEntity
}
val parseActivity = try {
apService.parseActivity(body.decodeToString())
} catch (e: Exception) {
LOGGER.warn("FAILED Parse Activity", e)
return ResponseEntity.accepted().build()
}
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
try {
val url = httpServletRequest.requestURL.toString()
val headers =
headersList.associateWith { header ->
httpServletRequest.getHeaders(header)?.toList().orEmpty()
}
apService.processActivity(
body.decodeToString(),
parseActivity,
HttpRequest(
URL(url + httpServletRequest.queryString.orEmpty()),
HttpHeaders(headers),
HttpMethod.POST
),
headers
)
} catch (e: Exception) {
LOGGER.warn("FAILED Process Activity $parseActivity", e)
return ResponseEntity(HttpStatus.ACCEPTED)
}
LOGGER.info("SUCCESS Processing Activity Type: {}", parseActivity)
return ResponseEntity(HttpStatus.ACCEPTED)
}
private fun checkHeader(
httpServletRequest: HttpServletRequest,
body: ByteArray,
): ResponseEntity<String>? {
try {
httpSignatureHeaderChecker.checkDate(httpServletRequest.getHeader("date")!!)
} catch (_: NullPointerException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Required date header")
} catch (_: IllegalArgumentException) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Request is too old.")
}
try {
httpSignatureHeaderChecker.checkHost(httpServletRequest.getHeader("host")!!)
} catch (_: NullPointerException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Required host header")
} catch (_: IllegalArgumentException) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Wrong host for request")
}
try {
httpSignatureHeaderChecker.checkDigest(body, httpServletRequest.getHeader("digest")!!)
} catch (_: NullPointerException) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Required request body digest in digest header (sha256)")
} catch (_: IllegalArgumentException) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("Wrong digest for request")
}
if (httpServletRequest.getHeader("signature").orEmpty().isBlank()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.header(
WWW_AUTHENTICATE,
"Signature realm=\"Example\",headers=\"(request-target) date host digest\""
)
.build()
}
return null
}
companion object {
private val LOGGER = LoggerFactory.getLogger(InboxControllerImpl::class.java)
}
}

View File

@ -1,82 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.nodeinfo
import dev.usbharu.hideout.activitypub.domain.model.nodeinfo.Nodeinfo
import dev.usbharu.hideout.activitypub.domain.model.nodeinfo.Nodeinfo2_0
import dev.usbharu.hideout.application.config.ApplicationConfig
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class NodeinfoController(private val applicationConfig: ApplicationConfig) {
@GetMapping("/.well-known/nodeinfo")
fun nodeinfo(): ResponseEntity<Nodeinfo> {
return ResponseEntity(
Nodeinfo(
listOf(
Nodeinfo.Links(
"http://nodeinfo.diaspora.software/ns/schema/2.0",
"${applicationConfig.url}/nodeinfo/2.0"
)
)
),
HttpStatus.OK
)
}
@GetMapping("/nodeinfo/2.0")
@Suppress("FunctionNaming")
fun nodeinfo2_0(): ResponseEntity<Nodeinfo2_0> {
return ResponseEntity(
Nodeinfo2_0(
version = "2.0",
software = Nodeinfo2_0.Software(
name = "hideout",
version = "0.0.1"
),
protocols = listOf("activitypub"),
services = Nodeinfo2_0.Services(
inbound = emptyList(),
outbound = emptyList()
),
openRegistrations = false,
usage = Nodeinfo2_0.Usage(
users = Nodeinfo2_0.Usage.Users(
total = 1,
activeHalfYear = 1,
activeMonth = 1
),
localPosts = 1,
localComments = 0
),
metadata = Nodeinfo2_0.Metadata(
nodeName = "hideout",
nodeDescription = "hideout test server",
maintainer = Nodeinfo2_0.Metadata.Maintainer("usbharu", "i@usbharu.dev"),
langs = emptyList(),
tosUrl = "",
repositoryUrl = "https://github.com/usbharu/Hideout",
feedbackUrl = "https://github.com/usbharu/Hideout/issues/new/choose",
)
),
HttpStatus.OK
)
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.note
import dev.usbharu.hideout.activitypub.domain.model.Note
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
interface NoteApController {
@GetMapping("/users/*/posts/{postId}")
suspend fun postsAp(
@PathVariable("postId") postId: Long
): ResponseEntity<Note>
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.note
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.service.objects.note.NoteApApiService
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUser
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController {
override suspend fun postsAp(
@PathVariable(value = "postId") postId: Long,
): ResponseEntity<Note> {
val context = SecurityContextHolder.getContext()
val userId =
if (context.authentication is PreAuthenticatedAuthenticationToken &&
context.authentication.details is HttpSignatureUser
) {
(context.authentication.details as HttpSignatureUser).id
} else {
null
}
val note = noteApApiService.getNote(postId, userId)
if (note != null) {
return ResponseEntity.ok(note)
}
return ResponseEntity.notFound().build()
}
}

View File

@ -1,28 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.outbox
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
@RestController
interface OutboxController {
@RequestMapping("/outbox", "/users/{username}/outbox", method = [RequestMethod.POST, RequestMethod.GET])
suspend fun outbox(): ResponseEntity<Unit>
}

View File

@ -1,27 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.outbox
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RestController
@RestController
class OutboxControllerImpl : OutboxController {
override suspend fun outbox(): ResponseEntity<Unit> =
ResponseEntity(HttpStatus.NOT_IMPLEMENTED)
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.interfaces.api.webfinger
import dev.usbharu.hideout.activitypub.domain.model.webfinger.WebFinger
import dev.usbharu.hideout.activitypub.service.webfinger.WebFingerApiService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.util.AcctUtil
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
@Controller
class WebFingerController(
private val webFingerApiService: WebFingerApiService,
private val applicationConfig: ApplicationConfig
) {
@GetMapping("/.well-known/webfinger")
fun webfinger(@RequestParam("resource") resource: String): ResponseEntity<WebFinger> = runBlocking {
logger.info("WEBFINGER Lookup webfinger resource: {}", resource)
val acct = try {
AcctUtil.parse(resource.replace("acct:", ""))
} catch (e: IllegalArgumentException) {
logger.warn("FAILED Parse acct.", e)
return@runBlocking ResponseEntity.badRequest().build()
}
val user = try {
webFingerApiService.findByNameAndDomain(acct.username, acct.domain ?: applicationConfig.url.host)
} catch (_: UserNotFoundException) {
return@runBlocking ResponseEntity.notFound().build()
}
val webFinger = WebFinger(
"acct:${user.name}@${user.domain}",
listOf(
WebFinger.Link(
"self",
"application/activity+json",
user.url
)
)
)
logger.info("SUCCESS Lookup webfinger resource: {} acct: {}", resource, acct)
ResponseEntity(webFinger, HttpStatus.OK)
}
companion object {
private val logger = LoggerFactory.getLogger(WebFingerController::class.java)
}
}

View File

@ -1,27 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.query
import dev.usbharu.hideout.activitypub.domain.model.Announce
import dev.usbharu.hideout.core.domain.model.post.Post
import org.springframework.stereotype.Repository
@Repository
interface AnnounceQueryService {
suspend fun findById(id: Long): Pair<Announce, Post>?
suspend fun findByApId(apId: String): Pair<Announce, Post>?
}

View File

@ -1,25 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.query
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.core.domain.model.post.Post
interface NoteQueryService {
suspend fun findById(id: Long): Pair<Note, Post>?
suspend fun findByApid(apId: String): Pair<Note, Post>?
}

View File

@ -1,62 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.accept
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.service.relationship.RelationshipService
import org.springframework.stereotype.Service
@Service
class ApAcceptProcessor(
transaction: Transaction,
private val relationshipService: RelationshipService,
private val actorRepository: ActorRepository
) :
AbstractActivityPubProcessor<Accept>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Accept>) {
val value = activity.activity.apObject
if (value.type.contains("Follow").not()) {
logger.warn("FAILED Activity type isn't Follow.")
throw IllegalActivityPubObjectException("Invalid type ${value.type}")
}
val follow = value as Follow
val userUrl = follow.apObject
val followerUrl = follow.actor
val user = actorRepository.findByUrl(userUrl) ?: throw UserNotFoundException.withUrl(userUrl)
val follower = actorRepository.findByUrl(followerUrl) ?: throw UserNotFoundException.withUrl(followerUrl)
relationshipService.acceptFollowRequest(user.id, follower.id)
logger.debug("SUCCESS Follow from ${user.url} to ${follower.url}.")
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Accept
override fun type(): Class<Accept> = Accept::class.java
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.accept
import dev.usbharu.hideout.activitypub.domain.model.Accept
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.external.job.DeliverAcceptTask
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
interface ApSendAcceptService {
suspend fun sendAcceptFollow(actor: Actor, target: Actor)
}
@Service
class ApSendAcceptServiceImpl(
private val owlProducer: OwlProducer,
) : ApSendAcceptService {
override suspend fun sendAcceptFollow(actor: Actor, target: Actor) {
val deliverAcceptTask = DeliverAcceptTask(
Accept(
apObject = Follow(
apObject = actor.url,
actor = target.url
),
actor = actor.url
),
target.inbox,
actor.id
)
owlProducer.publishTask(deliverAcceptTask)
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.announce
import dev.usbharu.hideout.activitypub.domain.model.Announce
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.application.external.Transaction
import org.springframework.stereotype.Service
@Service
class ApAnnounceProcessor(transaction: Transaction, private val apNoteService: APNoteService) :
AbstractActivityPubProcessor<Announce>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Announce>) {
apNoteService.fetchAnnounce(activity.activity)
}
override fun isSupported(activityType: ActivityType): Boolean = ActivityType.Announce == activityType
override fun type(): Class<Announce> = Announce::class.java
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.create
import dev.usbharu.hideout.core.domain.model.post.Post
interface ApSendCreateService {
suspend fun createNote(post: Post)
}

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.create
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.external.job.DeliverCreateTask
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.owl.producer.api.OwlProducer
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class ApSendCreateServiceImpl(
private val followerQueryService: FollowerQueryService,
private val noteQueryService: NoteQueryService,
private val applicationConfig: ApplicationConfig,
private val actorRepository: ActorRepository,
private val owlProducer: OwlProducer,
) : ApSendCreateService {
override suspend fun createNote(post: Post) {
logger.info("CREATE Create Local Note ${post.url}")
logger.debug("START Create Local Note ${post.url}")
logger.trace("{}", post)
val followers = followerQueryService.findFollowersById(post.actorId)
logger.debug("DELIVER Deliver Note Create ${followers.size} accounts.")
val userEntity = actorRepository.findById(post.actorId) ?: throw UserNotFoundException.withId(post.actorId)
val note = noteQueryService.findById(post.id)?.first ?: throw PostNotFoundException.withId(post.id)
val create = Create(
name = "Create Note",
apObject = note,
actor = note.attributedTo,
id = "${applicationConfig.url}/create/note/${post.id}"
)
followers.forEach { followerEntity ->
owlProducer.publishTask(DeliverCreateTask(create, userEntity.url, followerEntity.inbox))
}
logger.debug("SUCCESS Create Local Note ${post.url}")
}
companion object {
private val logger = LoggerFactory.getLogger(ApSendCreateServiceImpl::class.java)
}
}

View File

@ -1,38 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.create
import dev.usbharu.hideout.activitypub.domain.model.Create
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.application.external.Transaction
import org.springframework.stereotype.Service
@Service
class CreateActivityProcessor(transaction: Transaction, private val apNoteService: APNoteService) :
AbstractActivityPubProcessor<Create>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Create>) {
apNoteService.fetchNote(activity.activity.apObject as Note)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Create
override fun type(): Class<Create> = Create::class.java
}

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.delete
import dev.usbharu.hideout.activitypub.domain.exception.IllegalActivityPubObjectException
import dev.usbharu.hideout.activitypub.domain.model.Delete
import dev.usbharu.hideout.activitypub.domain.model.HasId
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectValue
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.service.post.PostService
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service
@Service
class APDeleteProcessor(
transaction: Transaction,
private val userService: UserService,
private val postService: PostService,
private val actorRepository: ActorRepository,
private val postRepository: PostRepository
) :
AbstractActivityPubProcessor<Delete>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Delete>) {
val value = activity.activity.apObject
val deleteId = if (value is HasId) {
value.id
} else if (value is ObjectValue) {
value.`object`
} else {
throw IllegalActivityPubObjectException("object hasn't id or object")
}
val actor = actorRepository.findByUrl(deleteId)
actor?.let { userService.deleteRemoteActor(it.id) }
val post = postRepository.findByApId(deleteId)
if (post == null) {
logger.warn("FAILED Delete id: {} is not found.", deleteId)
return
}
postService.deleteRemote(post)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Delete
override fun type(): Class<Delete> = Delete::class.java
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.delete
import dev.usbharu.hideout.activitypub.domain.model.Delete
import dev.usbharu.hideout.activitypub.domain.model.Tombstone
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectValue
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.external.job.DeliverDeleteTask
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
import java.time.Instant
interface APSendDeleteService {
suspend fun sendDeleteNote(deletedPost: Post)
suspend fun sendDeleteActor(deletedActor: Actor)
}
@Service
class APSendDeleteServiceImpl(
private val followerQueryService: FollowerQueryService,
private val applicationConfig: ApplicationConfig,
private val actorRepository: ActorRepository,
private val owlProducer: OwlProducer,
) : APSendDeleteService {
override suspend fun sendDeleteNote(deletedPost: Post) {
val actor =
actorRepository.findById(deletedPost.actorId) ?: throw UserNotFoundException.withId(deletedPost.actorId)
val followersById = followerQueryService.findFollowersById(deletedPost.actorId)
val delete = Delete(
actor = actor.url,
id = "${applicationConfig.url}/delete/note/${deletedPost.id}",
published = Instant.now().toString(),
`object` = Tombstone(id = deletedPost.apId)
)
followersById.forEach {
val jobProps = DeliverDeleteTask(
delete,
it.inbox,
actor.id
)
owlProducer.publishTask(jobProps)
}
}
override suspend fun sendDeleteActor(deletedActor: Actor) {
val followers = followerQueryService.findFollowersById(deletedActor.id)
val delete = Delete(
actor = deletedActor.url,
`object` = ObjectValue(emptyList(), `object` = deletedActor.url),
id = "${applicationConfig.url}/delete/actor/${deletedActor.id}",
published = Instant.now().toString()
)
followers.forEach {
DeliverDeleteTask(
delete = delete,
it.inbox,
deletedActor.id
)
}
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.follow
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.external.job.ReceiveFollowTask
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
@Service
class APFollowProcessor(
transaction: Transaction,
private val owlProducer: OwlProducer,
) :
AbstractActivityPubProcessor<Follow>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Follow>) {
logger.info("FOLLOW from: {} to {}", activity.activity.actor, activity.activity.apObject)
// inboxをジョブキューに乗せているので既に不要だが、フォロー承認制アカウントを実装する際に必要なので残す
val jobProps = ReceiveFollowTask(
activity.activity.actor,
activity.activity,
activity.activity.apObject
)
owlProducer.publishTask(jobProps)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Follow
override fun type(): Class<Follow> = Follow::class.java
}

View File

@ -1,42 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.follow
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.core.external.job.ReceiveFollowTask
import dev.usbharu.owl.producer.api.OwlProducer
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
interface APReceiveFollowService {
suspend fun receiveFollow(follow: Follow)
}
@Service
class APReceiveFollowServiceImpl(
private val owlProducer: OwlProducer,
) : APReceiveFollowService {
override suspend fun receiveFollow(follow: Follow) {
logger.info("FOLLOW from: {} to: {}", follow.actor, follow.apObject)
owlProducer.publishTask(ReceiveFollowTask(follow.actor, follow, follow.apObject))
return
}
companion object {
private val logger = LoggerFactory.getLogger(APReceiveFollowServiceImpl::class.java)
}
}

View File

@ -1,43 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.follow
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.service.common.APRequestService
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.service.follow.SendFollowDto
import org.springframework.stereotype.Service
interface APSendFollowService {
suspend fun sendFollow(sendFollowDto: SendFollowDto)
}
@Service
class APSendFollowServiceImpl(
private val apRequestService: APRequestService,
private val applicationConfig: ApplicationConfig,
) : APSendFollowService {
override suspend fun sendFollow(sendFollowDto: SendFollowDto) {
val follow = Follow(
apObject = sendFollowDto.followTargetActorId.url,
actor = sendFollowDto.actorId.url,
id = "${applicationConfig.url}/follow/${sendFollowDto.actorId.id}/${sendFollowDto.followTargetActorId.id}"
)
apRequestService.apPost(sendFollowDto.followTargetActorId.inbox, follow, sendFollowDto.actorId)
}
}

View File

@ -1,77 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.model.Emoji
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
import dev.usbharu.hideout.activitypub.service.objects.note.APNoteService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.service.reaction.ReactionService
import org.springframework.stereotype.Service
@Service
class APLikeProcessor(
transaction: Transaction,
private val apUserService: APUserService,
private val apNoteService: APNoteService,
private val reactionService: ReactionService,
private val emojiService: EmojiService
) :
AbstractActivityPubProcessor<Like>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Like>) {
val actor = activity.activity.actor
val content = activity.activity.content
val target = activity.activity.apObject
val personWithEntity = apUserService.fetchPersonWithEntity(actor)
try {
val post = apNoteService.fetchNoteWithEntity(target).second
val emoji = if (content.startsWith(":")) {
val tag = activity.activity.tag
(tag.firstOrNull { it is Emoji } as? Emoji)?.let { emojiService.fetchEmoji(it).second }
} else {
UnicodeEmoji(content)
}
reactionService.receiveReaction(
emoji ?: UnicodeEmoji(""),
personWithEntity.second.id,
post.id
)
logger.debug("SUCCESS Add Like($content) from ${personWithEntity.second.url} to ${post.url}")
} catch (e: FailedToGetActivityPubResourceException) {
logger.debug("FAILED failed to get {}", target)
logger.trace("", e)
return
}
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Like
override fun type(): Class<Like> = Like::class.java
}

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.like
import dev.usbharu.hideout.activitypub.domain.model.Like
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.external.job.DeliverReactionTask
import dev.usbharu.hideout.core.external.job.DeliverUndoTask
import dev.usbharu.hideout.core.query.FollowerQueryService
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
import java.time.Instant
interface APReactionService {
suspend fun reaction(like: Reaction)
suspend fun removeReaction(like: Reaction)
}
@Service
class APReactionServiceImpl(
private val followerQueryService: FollowerQueryService,
private val actorRepository: ActorRepository,
private val postRepository: PostRepository,
private val applicationConfig: ApplicationConfig,
private val owlProducer: OwlProducer,
) : APReactionService {
override suspend fun reaction(like: Reaction) {
val followers = followerQueryService.findFollowersById(like.actorId)
val user = actorRepository.findById(like.actorId) ?: throw UserNotFoundException.withId(like.actorId)
val post =
postRepository.findById(like.postId) ?: throw PostNotFoundException.withId(like.postId)
followers.forEach { follower ->
owlProducer.publishTask(
DeliverReactionTask(
actor = user.url,
like = Like(
actor = user.url,
id = "${applicationConfig.url}/like/note/${post.id}",
content = "",
apObject = post.url
),
inbox = follower.inbox
)
)
}
}
override suspend fun removeReaction(like: Reaction) {
val followers = followerQueryService.findFollowersById(like.actorId)
val user = actorRepository.findById(like.actorId) ?: throw UserNotFoundException.withId(like.actorId)
val post =
postRepository.findById(like.postId) ?: throw PostNotFoundException.withId(like.postId)
followers.forEach { follower ->
owlProducer.publishTask(
DeliverUndoTask(
signer = user.id,
inbox = follower.inbox,
undo = Undo(
actor = user.url,
id = "${applicationConfig.url}/undo/like/${post.id}",
apObject = Like(
actor = user.url,
id = "${applicationConfig.url}/like/note/${post.id}",
content = "",
apObject = post.url
),
published = Instant.now().toString(),
)
)
)
}
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.reject
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.domain.model.Reject
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.service.relationship.RelationshipService
import org.springframework.stereotype.Service
@Service
class ApRejectProcessor(
private val relationshipService: RelationshipService,
transaction: Transaction,
private val actorRepository: ActorRepository
) :
AbstractActivityPubProcessor<Reject>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Reject>) {
val activityType = activity.activity.apObject.type.firstOrNull { it == "Follow" }
if (activityType == null) {
logger.warn("FAILED Process Reject Activity type: {}", activity.activity.apObject.type)
return
}
when (activityType) {
"Follow" -> {
val user = actorRepository.findByUrl(activity.activity.actor) ?: throw UserNotFoundException.withUrl(
activity.activity.actor
)
activity.activity.apObject as Follow
val actor = activity.activity.apObject.actor
val target = actorRepository.findByUrl(actor) ?: throw UserNotFoundException.withUrl(actor)
logger.debug("REJECT Follow user {} target {}", user.url, target.url)
relationshipService.rejectFollowRequest(user.id, target.id)
}
else -> {}
}
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Reject
override fun type(): Class<Reject> = Reject::class.java
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.reject
import dev.usbharu.hideout.core.domain.model.actor.Actor
interface ApSendRejectService {
suspend fun sendRejectFollow(actor: Actor, target: Actor)
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.reject
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.domain.model.Reject
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.external.job.DeliverRejectTask
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
@Service
class ApSendRejectServiceImpl(
private val applicationConfig: ApplicationConfig,
private val owlProducer: OwlProducer,
) : ApSendRejectService {
override suspend fun sendRejectFollow(actor: Actor, target: Actor) {
val deliverRejectTask = DeliverRejectTask(
Reject(
actor.url,
"${applicationConfig.url}/reject/${actor.id}/${target.id}",
Follow(apObject = actor.url, actor = target.url)
),
target.inbox,
actor.id
)
owlProducer.publishTask(deliverRejectTask)
}
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.undo
import dev.usbharu.hideout.core.domain.model.actor.Actor
interface APSendUndoService {
suspend fun sendUndoFollow(actor: Actor, target: Actor)
}

View File

@ -1,50 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.undo
import dev.usbharu.hideout.activitypub.domain.model.Follow
import dev.usbharu.hideout.activitypub.domain.model.Undo
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.external.job.DeliverUndoTask
import dev.usbharu.owl.producer.api.OwlProducer
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class APSendUndoServiceImpl(
private val applicationConfig: ApplicationConfig,
private val owlProducer: OwlProducer,
) : APSendUndoService {
override suspend fun sendUndoFollow(actor: Actor, target: Actor) {
val deliverUndoTask = DeliverUndoTask(
Undo(
actor = actor.url,
id = "${applicationConfig.url}/undo/follow/${actor.id}/${target.url}",
apObject = Follow(
apObject = actor.url,
actor = target.url
),
published = Instant.now().toString()
),
target.inbox,
actor.id
)
owlProducer.publishTask(deliverUndoTask)
}
}

View File

@ -1,145 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.activity.undo
import dev.usbharu.hideout.activitypub.domain.model.*
import dev.usbharu.hideout.activitypub.domain.model.objects.ObjectValue
import dev.usbharu.hideout.activitypub.service.common.AbstractActivityPubProcessor
import dev.usbharu.hideout.activitypub.service.common.ActivityPubProcessContext
import dev.usbharu.hideout.activitypub.service.common.ActivityType
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.PostNotFoundException
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.exception.resource.local.LocalUserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.service.post.PostService
import dev.usbharu.hideout.core.service.reaction.ReactionService
import dev.usbharu.hideout.core.service.relationship.RelationshipService
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service
@Service
class APUndoProcessor(
transaction: Transaction,
private val apUserService: APUserService,
private val relationshipService: RelationshipService,
private val reactionService: ReactionService,
private val actorRepository: ActorRepository,
private val postRepository: PostRepository,
private val postService: PostService,
private val userService: UserService,
) : AbstractActivityPubProcessor<Undo>(transaction) {
override suspend fun internalProcess(activity: ActivityPubProcessContext<Undo>) {
val undo = activity.activity
val type = undo.apObject.type.firstOrNull {
it == "Block" || it == "Follow" || it == "Like" || it == "Announce" || it == "Accept"
} ?: return
when (type) {
"Follow" -> {
follow(undo)
return
}
"Accept" -> {
accept(undo)
return
}
"Like" -> {
like(undo)
return
}
"Announce" -> {
announce(undo)
return
}
"Delete" -> {
delete(undo)
return
}
else -> {}
}
TODO()
}
private suspend fun accept(undo: Undo) {
val accept = undo.apObject as Accept
val acceptObject = if (accept.apObject is ObjectValue) {
accept.apObject.`object`
} else if (accept.apObject is Follow) {
accept.apObject.apObject
} else {
logger.warn("FAILED Unsupported type. Undo Accept {}", accept.apObject.type)
return
}
val accepter = apUserService.fetchPersonWithEntity(undo.actor, acceptObject).second
val target = actorRepository.findByUrl(acceptObject) ?: throw UserNotFoundException.withUrl(acceptObject)
relationshipService.rejectFollowRequest(accepter.id, target.id)
return
}
private suspend fun like(undo: Undo) {
val like = undo.apObject as Like
val post = postRepository.findByUrl(like.apObject) ?: throw PostNotFoundException.withUrl(like.apObject)
val signer = actorRepository.findById(post.actorId) ?: throw LocalUserNotFoundException.withId(post.actorId)
val actor = apUserService.fetchPersonWithEntity(like.actor, signer.url).second
reactionService.receiveRemoveReaction(actor.id, post.id)
return
}
private suspend fun follow(undo: Undo) {
val follow = undo.apObject as Follow
val follower = apUserService.fetchPersonWithEntity(undo.actor, follow.apObject).second
val target = actorRepository.findByUrl(follow.apObject) ?: throw UserNotFoundException.withUrl(follow.apObject)
relationshipService.unfollow(follower.id, target.id)
return
}
private suspend fun announce(undo: Undo) {
val announce = undo.apObject as Announce
val findByApId = postRepository.findByApId(announce.id) ?: return
postService.deleteRemote(findByApId)
}
private suspend fun delete(undo: Undo) {
val announce = undo.apObject as Delete
val actor = actorRepository.findByUrl(announce.actor) ?: throw UserNotFoundException.withUrl(announce.actor)
userService.restorationRemoteActor(actor.id)
}
override fun isSupported(activityType: ActivityType): Boolean = activityType == ActivityType.Undo
override fun type(): Class<Undo> = Undo::class.java
}

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.core.domain.model.actor.Actor
interface APRequestService {
suspend fun <R : Object> apGet(url: String, signer: Actor? = null, responseClass: Class<R>): R
suspend fun <T : Object, R : Object> apPost(
url: String,
body: T? = null,
signer: Actor? = null,
responseClass: Class<R>
): R
suspend fun <T : Object> apPost(url: String, body: T? = null, signer: Actor? = null): String
}
suspend inline fun <reified R : Object> APRequestService.apGet(url: String, signer: Actor? = null): R =
apGet(url, signer, R::class.java)
suspend inline fun <T : Object, reified R : Object> APRequestService.apPost(
url: String,
body: T? = null,
signer: Actor? = null
): R = apPost(url, body, signer, R::class.java)

View File

@ -1,251 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.activitypub.domain.Constant
import dev.usbharu.hideout.activitypub.domain.model.StringOrObject
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.util.Base64Util
import dev.usbharu.hideout.util.HttpUtil.Activity
import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.common.PrivateKey
import dev.usbharu.httpsignature.sign.HttpSignatureSigner
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import java.net.URL
import java.security.MessageDigest
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Service
class APRequestServiceImpl(
private val httpClient: HttpClient,
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val httpSignatureSigner: HttpSignatureSigner,
@Qualifier("http") private val dateTimeFormatter: DateTimeFormatter,
) : APRequestService {
override suspend fun <R : Object> apGet(url: String, signer: Actor?, responseClass: Class<R>): R {
logger.debug("START ActivityPub Request GET url: {}, signer: {}", url, signer?.url)
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url)
val httpResponse = if (signer?.privateKey == null) {
apGetNotSign(url, date)
} else {
apGetSign(date, u, signer, url)
}
val bodyAsText = httpResponse.bodyAsText()
val readValue = objectMapper.readValue(bodyAsText, responseClass)
logger.debug(
"SUCCESS ActivityPub Request GET status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url)
return readValue
}
private suspend fun apGetSign(
date: String,
u: URL,
signer: Actor,
url: String,
): HttpResponse {
val headers = headers {
append("Accept", Activity)
append("Date", date)
append("Host", u.host)
}
val sign = httpSignatureSigner.sign(
httpRequest = HttpRequest(
url = u,
headers = HttpHeaders(headers.toMap()),
HttpMethod.GET
),
privateKey = PrivateKey(
keyId = "${signer.url}#pubkey",
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!),
),
signHeaders = listOf("(request-target)", "date", "host", "accept")
)
val httpResponse = httpClient.get(url) {
headers {
headers {
appendAll(headers)
append("Signature", sign.signatureHeader)
// remove("Host")
}
}
contentType(Activity)
}
return httpResponse
}
private suspend fun apGetNotSign(url: String, date: String?) = httpClient.get(url) {
header("Accept", Activity)
header("Date", date)
}
override suspend fun <T : Object, R : Object> apPost(
url: String,
body: T?,
signer: Actor?,
responseClass: Class<R>,
): R {
val bodyAsText = apPost(url, body, signer)
return objectMapper.readValue(bodyAsText, responseClass)
}
override suspend fun <T : Object> apPost(url: String, body: T?, signer: Actor?): String {
logger.debug("START ActivityPub Request POST url: {}, signer: {}", url, signer?.url)
val requestBody = addContextIfNotNull(body)
logger.trace(
"""
|
|***** BEGIN HTTP Request Trace url: {} *****
|
|$requestBody
|
|***** END HTTP Request Trace url: {} *****
|
""".trimMargin(),
url,
url
)
val sha256 = MessageDigest.getInstance("SHA-256")
val digest = Base64Util.encode(sha256.digest(requestBody.orEmpty().toByteArray()))
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
val u = URL(url)
val httpResponse = if (signer?.privateKey == null) {
apPostNotSign(url, date, digest, requestBody)
} else {
apPostSign(date, u, digest, signer, requestBody)
}
val bodyAsText = httpResponse.bodyAsText()
logger.debug(
"SUCCESS ActivityPub Request POST status: {} url: {}",
httpResponse.status,
httpResponse.request.url
)
logBody(bodyAsText, url)
return bodyAsText
}
private suspend fun apPostNotSign(
url: String,
date: String?,
digest: String,
requestBody: String?,
) = httpClient.post(url) {
accept(Activity)
header("Date", date)
header("Digest", "sha-256=$digest")
if (requestBody != null) {
setBody(requestBody)
contentType(Activity)
}
}
private suspend fun apPostSign(
date: String,
u: URL,
digest: String,
signer: Actor,
requestBody: String?,
): HttpResponse {
val headers = headers {
append("Accept", Activity)
append("Date", date)
append("Host", u.host)
append("Digest", "SHA-256=$digest")
}
val sign = httpSignatureSigner.sign(
httpRequest = HttpRequest(
u,
HttpHeaders(headers.toMap()),
HttpMethod.POST
),
privateKey = PrivateKey(
keyId = signer.keyId,
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey!!)
),
signHeaders = listOf("(request-target)", "date", "host", "digest")
)
val httpResponse = httpClient.post(u) {
headers {
appendAll(headers)
append("Signature", sign.signatureHeader)
// remove("Host")
}
setBody(requestBody)
contentType(Activity)
}
return httpResponse
}
private fun <T : Object> addContextIfNotNull(body: T?) = if (body != null) {
val context = mutableListOf<StringOrObject>()
context.addAll(Constant.context)
context.addAll(body.context)
body.context = context
objectMapper.writeValueAsString(body)
} else {
null
}
private fun logBody(bodyAsText: String, url: String) {
logger.trace(
"""
|
|***** BEGIN HTTP Response Trace url: {} *****
|
|$bodyAsText
|
|***** END HTTP Response TRACE url: {} *****
|
""".trimMargin(),
url,
url
)
}
companion object {
private val logger = LoggerFactory.getLogger(APRequestServiceImpl::class.java)
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.core.domain.model.actor.Actor
interface APResourceResolveService {
suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singer: Actor?): T
suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singerId: Long?): T
}
suspend inline fun <reified T : Object> APResourceResolveService.resolve(url: String, singer: Actor?): T =
resolve(url, T::class.java, singer)
suspend inline fun <reified T : Object> APResourceResolveService.resolve(url: String, singerId: Long?): T =
resolve(url, T::class.java, singerId)

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.service.resource.CacheManager
import dev.usbharu.hideout.core.service.resource.ResolveResponse
import org.springframework.stereotype.Service
import java.io.InputStream
@Service
class APResourceResolveServiceImpl(
private val apRequestService: APRequestService,
private val actorRepository: ActorRepository,
private val cacheManager: CacheManager
) :
APResourceResolveService {
override suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singerId: Long?): T =
internalResolve(url, singerId, clazz)
override suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singer: Actor?): T =
internalResolve(url, singer, clazz)
private suspend fun <T : Object> internalResolve(url: String, singerId: Long?, clazz: Class<T>): T {
val key = genCacheKey(url, singerId)
cacheManager.putCache(key) {
runResolve(url, singerId?.let { actorRepository.findById(it) }, clazz)
}
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
}
private suspend fun <T : Object> internalResolve(url: String, singer: Actor?, clazz: Class<T>): T {
val key = genCacheKey(url, singer?.id)
cacheManager.putCache(key) {
runResolve(url, singer, clazz)
}
return (cacheManager.getOrWait(key) as APResolveResponse<T>).objects
}
private suspend fun <T : Object> runResolve(url: String, singer: Actor?, clazz: Class<T>): ResolveResponse =
APResolveResponse(apRequestService.apGet(url, singer, clazz))
private fun genCacheKey(url: String, singerId: Long?): String {
if (singerId != null) {
return "$url-$singerId"
}
return url
}
private class APResolveResponse<T : Object>(val objects: T) : ResolveResponse {
override suspend fun body(): InputStream {
TODO("Not yet implemented")
}
override suspend fun bodyAsText(): String {
TODO("Not yet implemented")
}
override suspend fun bodyAsBytes(): ByteArray {
TODO("Not yet implemented")
}
override suspend fun header(): Map<String, List<String>> {
TODO("Not yet implemented")
}
override suspend fun status(): Int {
TODO("Not yet implemented")
}
override suspend fun statusMessage(): String {
TODO("Not yet implemented")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as APResolveResponse<*>
return objects == other.objects
}
override fun hashCode(): Int = objects.hashCode()
}
}

View File

@ -1,103 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import dev.usbharu.hideout.activitypub.domain.exception.JsonParseException
import dev.usbharu.hideout.core.external.job.InboxTask
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.owl.producer.api.OwlProducer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
interface APService {
fun parseActivity(json: String): ActivityType
suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
)
}
@Service
class APServiceImpl(
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val owlProducer: OwlProducer,
) : APService {
val logger: Logger = LoggerFactory.getLogger(APServiceImpl::class.java)
override fun parseActivity(json: String): ActivityType {
val readTree = try {
objectMapper.readTree(json)
} catch (e: com.fasterxml.jackson.core.JsonParseException) {
throw JsonParseException("Failed to parse json", e)
}
logger.trace(
"""
|
|***** Trace Begin Activity *****
|
|{}
|
|***** Trace End Activity *****
|
""".trimMargin(),
readTree.toPrettyString()
)
if (readTree.isObject.not()) {
throw JsonParseException("Json is not object.")
}
val type = readTree["type"] ?: throw JsonParseException("Type is null")
if (type.isArray) {
try {
return type.firstNotNullOf { jsonNode: JsonNode ->
ActivityType.entries.firstOrNull { it.name.equals(jsonNode.asText(), true) }
}
} catch (e: NoSuchElementException) {
throw IllegalArgumentException("No valid TYPE", e)
}
}
try {
return ActivityType.entries.first { it.name.equals(type.asText(), true) }
} catch (e: NoSuchElementException) {
throw IllegalArgumentException("No valid TYPE", e)
}
}
override suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
) {
logger.debug("process activity: {}", type)
owlProducer.publishTask(
InboxTask(
json,
type,
httpRequest,
map
)
)
return
}
}

View File

@ -1,56 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.exception.ActivityPubProcessException
import dev.usbharu.hideout.activitypub.domain.exception.FailedProcessException
import dev.usbharu.hideout.activitypub.domain.exception.HttpSignatureUnauthorizedException
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.ResourceAccessException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.sql.SQLException
abstract class AbstractActivityPubProcessor<T : Object>(
private val transaction: Transaction,
private val allowUnauthorized: Boolean = false
) : ActivityPubProcessor<T> {
protected val logger: Logger = LoggerFactory.getLogger(this::class.java)
override suspend fun process(activity: ActivityPubProcessContext<T>) {
if (activity.isAuthorized.not() && allowUnauthorized.not()) {
throw HttpSignatureUnauthorizedException()
}
logger.info("START ActivityPub process. {}", this.type())
try {
transaction.transaction {
try {
internalProcess(activity)
} catch (e: ResourceAccessException) {
throw SQLException(e)
}
}
} catch (e: ActivityPubProcessException) {
logger.warn("FAILED ActivityPub process", e)
throw FailedProcessException("Failed process", e)
}
logger.info("SUCCESS ActivityPub process. {}", this.type())
}
abstract suspend fun internalProcess(activity: ActivityPubProcessContext<T>)
}

View File

@ -1,30 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import com.fasterxml.jackson.databind.JsonNode
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.verify.Signature
data class ActivityPubProcessContext<T : Object>(
val activity: T,
val jsonNode: JsonNode,
val httpRequest: HttpRequest,
val signature: Signature?,
val isAuthorized: Boolean
)

View File

@ -1,27 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
import dev.usbharu.hideout.activitypub.domain.model.objects.Object
interface ActivityPubProcessor<T : Object> {
suspend fun process(activity: ActivityPubProcessContext<T>)
fun isSupported(activityType: ActivityType): Boolean
fun type(): Class<T>
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
enum class ActivityType {
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
Other
}

View File

@ -1,74 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
enum class ActivityVocabulary {
Object,
Link,
Activity,
IntransitiveActivity,
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
Application,
Group,
Organization,
Person,
Service,
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
Mention,
}

View File

@ -1,75 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
enum class ExtendedActivityVocabulary {
Object,
Link,
Activity,
IntransitiveActivity,
Collection,
OrderedCollection,
CollectionPage,
OrderedCollectionPage,
Accept,
Add,
Announce,
Arrive,
Block,
Create,
Delete,
Dislike,
Flag,
Follow,
Ignore,
Invite,
Join,
Leave,
Like,
Listen,
Move,
Offer,
Question,
Reject,
Read,
Remove,
TentativeReject,
TentativeAccept,
Travel,
Undo,
Update,
View,
Application,
Group,
Organization,
Person,
Service,
Article,
Audio,
Document,
Event,
Image,
Note,
Page,
Place,
Profile,
Relationship,
Tombstone,
Video,
Mention,
Emoji
}

View File

@ -1,21 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.common
enum class ExtendedVocabulary {
Emoji
}

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.emoji
import dev.usbharu.hideout.activitypub.domain.model.Emoji
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
interface EmojiService {
suspend fun fetchEmoji(url: String): Pair<Emoji, CustomEmoji>
suspend fun fetchEmoji(emoji: Emoji): Pair<Emoji, CustomEmoji>
suspend fun findByEmojiName(emojiName: String): CustomEmoji?
}

View File

@ -1,95 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.emoji
import dev.usbharu.hideout.activitypub.domain.model.Emoji
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveServiceImpl
import dev.usbharu.hideout.activitypub.service.common.resolve
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmoji
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
import dev.usbharu.hideout.core.service.instance.InstanceService
import dev.usbharu.hideout.core.service.media.MediaService
import dev.usbharu.hideout.core.service.media.RemoteMedia
import org.springframework.stereotype.Service
import java.net.URL
import java.time.Instant
@Service
class EmojiServiceImpl(
private val customEmojiRepository: CustomEmojiRepository,
private val instanceService: InstanceService,
private val mediaService: MediaService,
private val apResourceResolveServiceImpl: APResourceResolveServiceImpl,
private val applicationConfig: ApplicationConfig
) : EmojiService {
override suspend fun fetchEmoji(url: String): Pair<Emoji, CustomEmoji> {
val emoji = apResourceResolveServiceImpl.resolve<Emoji>(url, null as Long?)
return fetchEmoji(emoji)
}
override suspend fun fetchEmoji(emoji: Emoji): Pair<Emoji, CustomEmoji> = emoji to save(emoji)
private suspend fun save(emoji: Emoji): CustomEmoji {
val domain = URL(emoji.id).host
val name = emoji.name.trim(':')
val customEmoji = customEmojiRepository.findByNameAndDomain(name, domain)
if (customEmoji != null) {
return customEmoji
}
val instance = instanceService.fetchInstance(emoji.id)
val media = mediaService.uploadRemoteMedia(
RemoteMedia(
emoji.name,
emoji.icon.url,
emoji.icon.mediaType.orEmpty(),
null
)
)
val customEmoji1 = CustomEmoji(
id = customEmojiRepository.generateId(),
name = name,
domain = domain,
instanceId = instance.id,
url = media.url,
category = null,
createdAt = Instant.now()
)
return customEmojiRepository.save(customEmoji1)
}
override suspend fun findByEmojiName(emojiName: String): CustomEmoji? {
val split = emojiName.trim(':').split("@")
return when (split.size) {
1 -> {
customEmojiRepository.findByNameAndDomain(split.first(), applicationConfig.url.host)
}
2 -> {
customEmojiRepository.findByNameAndDomain(split.first(), split[1])
}
else -> throw IllegalArgumentException("Unknown Emoji Format. $emojiName")
}
}
}

View File

@ -1,275 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.note
import dev.usbharu.hideout.activitypub.domain.exception.FailedToGetActivityPubResourceException
import dev.usbharu.hideout.activitypub.domain.model.Announce
import dev.usbharu.hideout.activitypub.domain.model.Emoji
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.domain.model.Person
import dev.usbharu.hideout.activitypub.query.AnnounceQueryService
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
import dev.usbharu.hideout.activitypub.service.common.resolve
import dev.usbharu.hideout.activitypub.service.objects.emoji.EmojiService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.service.media.MediaService
import dev.usbharu.hideout.core.service.media.RemoteMedia
import dev.usbharu.hideout.core.service.post.PostService
import io.ktor.client.plugins.*
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
interface APNoteService {
suspend fun fetchNote(url: String, targetActor: String? = null): Note = fetchNoteWithEntity(url, targetActor).first
suspend fun fetchNote(note: Note, targetActor: String? = null): Note
suspend fun fetchNoteWithEntity(url: String, targetActor: String? = null): Pair<Note, Post>
suspend fun fetchAnnounce(url: String, signerId: Long? = null): Pair<Announce, Post>
suspend fun fetchAnnounce(announce: Announce, signerId: Long? = null): Pair<Announce, Post>
}
@Service
@Suppress("LongParameterList")
class APNoteServiceImpl(
private val postRepository: PostRepository,
private val apUserService: APUserService,
private val postService: PostService,
private val apResourceResolveService: APResourceResolveService,
private val postBuilder: Post.PostBuilder,
private val noteQueryService: NoteQueryService,
private val mediaService: MediaService,
private val emojiService: EmojiService,
private val announceQueryService: AnnounceQueryService
) : APNoteService {
private val logger = LoggerFactory.getLogger(APNoteServiceImpl::class.java)
override suspend fun fetchNoteWithEntity(url: String, targetActor: String?): Pair<Note, Post> {
logger.debug("START Fetch Note url: {}", url)
val post = noteQueryService.findByApid(url)
if (post != null) {
logger.debug("SUCCESS Found in local url: {}", url)
return post
}
logger.info("AP GET url: {}", url)
val note = try {
apResourceResolveService.resolve<Note>(url, null as Long?)
} catch (e: ClientRequestException) {
logger.warn(
"FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}",
e.response.status,
url
)
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
}
val savedNote = saveIfMissing(note, targetActor)
logger.debug("SUCCESS Fetch Note url: {}", url)
return savedNote
}
override suspend fun fetchAnnounce(url: String, signerId: Long?): Pair<Announce, Post> {
logger.debug("START Fetch Announce url: {}", url)
val post: Pair<Announce, Post>? = announceQueryService.findByApId(url)
if (post != null) {
logger.debug("SUCCESS Found in local url: {}", url)
return post
}
logger.info("AP GET url: {}", url)
val announce = try {
apResourceResolveService.resolve<Announce>(url, signerId)
} catch (e: ClientRequestException) {
logger.warn(
"FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}",
e.response.status,
url
)
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
}
return fetchAnnounce(announce, signerId)
}
override suspend fun fetchAnnounce(announce: Announce, signerId: Long?): Pair<Announce, Post> {
val findByApId = announceQueryService.findByApId(announce.id)
if (findByApId != null) {
return findByApId
}
val (_, actor) = apUserService.fetchPersonWithEntity(announce.actor, null)
val (_, post) = fetchNoteWithEntity(announce.apObject, null)
val visibility = if (announce.to.contains(public)) {
Visibility.PUBLIC
} else if (announce.to.contains(actor.followers) && announce.cc.contains(public)) {
Visibility.UNLISTED
} else if (announce.to.contains(actor.followers)) {
Visibility.FOLLOWERS
} else {
Visibility.DIRECT
}
val createRemote = postService.createRemote(
postBuilder.pureRepostOf(
id = postRepository.generateId(),
actorId = actor.id,
visibility = visibility,
createdAt = Instant.parse(announce.published),
url = announce.id,
repost = post,
apId = announce.id
)
)
return announce to createRemote
}
private suspend fun saveIfMissing(
note: Note,
targetActor: String?
): Pair<Note, Post> = noteQueryService.findByApid(note.id) ?: saveNote(note, targetActor)
private suspend fun saveNote(note: Note, targetActor: String?): Pair<Note, Post> {
val person = apUserService.fetchPersonWithEntity(
note.attributedTo,
targetActor
)
val post = postRepository.findByApId(note.id)
if (post != null) {
return note to post
}
logger.debug("VISIBILITY url: {} to: {} cc: {}", note.id, note.to, note.cc)
val visibility = visibility(note, person)
logger.debug("VISIBILITY is {} url: {}", visibility.name, note.id)
val reply = note.inReplyTo?.let {
fetchNote(it, targetActor)
postRepository.findByUrl(it)
}
val quote = (note.misskeyQuote ?: note.quoteUri ?: note.quoteUrl)?.let {
fetchNote(it, targetActor)
postRepository.findByUrl(it)
}
val emojis = buildEmojis(note)
val mediaList = note.attachment.map {
mediaService.uploadRemoteMedia(
RemoteMedia(
it.name,
it.url,
it.mediaType,
description = it.name
)
)
}.map { it.id }
val createPost =
post(quote, person, note, visibility, reply, mediaList, emojis)
val createRemote = postService.createRemote(createPost)
return note to createRemote
}
private suspend fun post(
quote: Post?,
person: Pair<Person, Actor>,
note: Note,
visibility: Visibility,
reply: Post?,
mediaList: List<Long>,
emojis: List<Long>
) = if (quote != null) {
postBuilder.quoteRepostOf(
id = postRepository.generateId(),
actorId = person.second.id,
content = note.content,
createdAt = Instant.parse(note.published),
visibility = visibility,
url = note.id,
replyId = reply?.id,
sensitive = note.sensitive,
apId = note.id,
mediaIds = mediaList,
emojiIds = emojis,
repost = quote
)
} else {
postBuilder.of(
id = postRepository.generateId(),
actorId = person.second.id,
content = note.content,
createdAt = Instant.parse(note.published).toEpochMilli(),
visibility = visibility,
url = note.id,
replyId = reply?.id,
sensitive = note.sensitive,
apId = note.id,
mediaIds = mediaList,
emojiIds = emojis
)
}
private suspend fun buildEmojis(note: Note) = note.tag
.filterIsInstance<Emoji>()
.map {
emojiService.fetchEmoji(it).second
}
.map {
it.id
}
private fun visibility(
note: Note,
person: Pair<Person, Actor>
): Visibility {
val visibility = if (note.to.contains(public)) {
Visibility.PUBLIC
} else if (note.to.contains(person.second.followers) && note.cc.contains(public)) {
Visibility.UNLISTED
} else if (note.to.contains(person.second.followers)) {
Visibility.FOLLOWERS
} else {
Visibility.DIRECT
}
return visibility
}
override suspend fun fetchNote(note: Note, targetActor: String?): Note =
saveIfMissing(note, targetActor).first
companion object {
const val public: String = "https://www.w3.org/ns/activitystreams#Public"
}
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.note
import dev.usbharu.hideout.activitypub.domain.model.Note
interface NoteApApiService {
suspend fun getNote(postId: Long, userId: Long?): Note?
}

View File

@ -1,72 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.note
import dev.usbharu.hideout.activitypub.domain.model.Note
import dev.usbharu.hideout.activitypub.query.NoteQueryService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.model.post.Post
import dev.usbharu.hideout.core.domain.model.post.Visibility
import dev.usbharu.hideout.core.query.FollowerQueryService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class NoteApApiServiceImpl(
private val noteQueryService: NoteQueryService,
private val followerQueryService: FollowerQueryService,
private val transaction: Transaction
) : NoteApApiService {
override suspend fun getNote(postId: Long, userId: Long?): Note? = transaction.transaction {
val findById = noteQueryService.findById(postId)
if (findById == null) {
logger.warn("Note not found. $postId $userId")
return@transaction null
}
when (findById.second.visibility) {
Visibility.PUBLIC, Visibility.UNLISTED -> {
return@transaction findById.first
}
Visibility.FOLLOWERS -> {
return@transaction getFollowersNote(userId, findById)
}
Visibility.DIRECT -> return@transaction null
}
}
private suspend fun getFollowersNote(
userId: Long?,
findById: Pair<Note, Post>
): Note? {
if (userId == null) {
return null
}
if (followerQueryService.alreadyFollow(findById.second.actorId, userId)) {
return findById.first
}
return null
}
companion object {
private val logger = LoggerFactory.getLogger(NoteApApiServiceImpl::class.java)
}
}

View File

@ -1,166 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.objects.user
import dev.usbharu.hideout.activitypub.domain.model.Image
import dev.usbharu.hideout.activitypub.domain.model.Key
import dev.usbharu.hideout.activitypub.domain.model.Person
import dev.usbharu.hideout.activitypub.service.common.APResourceResolveService
import dev.usbharu.hideout.activitypub.service.common.resolve
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.service.user.RemoteUserCreateDto
import dev.usbharu.hideout.core.service.user.UserService
import org.springframework.stereotype.Service
interface APUserService {
suspend fun getPersonByName(name: String): Person
/**
* Fetch person
*
* @param url
* @param targetActor 署名するユーザー
* @return
*/
suspend fun fetchPerson(url: String, targetActor: String? = null, idOverride: Long? = null): Person
suspend fun fetchPersonWithEntity(
url: String,
targetActor: String? = null,
idOverride: Long? = null,
): Pair<Person, Actor>
}
@Service
class APUserServiceImpl(
private val userService: UserService,
private val transaction: Transaction,
private val applicationConfig: ApplicationConfig,
private val apResourceResolveService: APResourceResolveService,
private val actorRepository: ActorRepository,
) :
APUserService {
override suspend fun getPersonByName(name: String): Person {
val userEntity = transaction.transaction {
actorRepository.findByNameAndDomain(name, applicationConfig.url.host)
?: throw UserNotFoundException.withNameAndDomain(name, applicationConfig.url.host)
}
// TODO: JOINで書き直し
val userUrl = "${applicationConfig.url}/users/$name"
return Person(
type = emptyList(),
name = userEntity.name,
id = userUrl,
preferredUsername = name,
summary = userEntity.description,
inbox = "$userUrl/inbox",
outbox = "$userUrl/outbox",
url = userUrl,
icon = Image(
type = emptyList(),
mediaType = "image/jpeg",
url = "$userUrl/icon.jpg"
),
publicKey = Key(
id = userEntity.keyId,
owner = userUrl,
publicKeyPem = userEntity.publicKey
),
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
followers = userEntity.followers,
following = userEntity.following,
manuallyApprovesFollowers = userEntity.locked
)
}
override suspend fun fetchPerson(url: String, targetActor: String?, idOverride: Long?): Person =
fetchPersonWithEntity(url, targetActor, idOverride).first
override suspend fun fetchPersonWithEntity(
url: String,
targetActor: String?,
idOverride: Long?,
): Pair<Person, Actor> {
val userEntity = actorRepository.findByUrl(url)
if (userEntity != null && idOverride == null) {
return entityToPerson(userEntity, userEntity.url) to userEntity
}
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
val id = person.id
val actor = actorRepository.findByUrlWithLock(id)
if (actor != null && idOverride == null) {
return person to actor
}
return person to userService.createRemoteUser(
RemoteUserCreateDto(
name = person.preferredUsername,
domain = id.substringAfter("://").substringBefore("/"),
screenName = person.name ?: person.preferredUsername,
description = person.summary.orEmpty(),
inbox = person.inbox,
outbox = person.outbox,
url = id,
publicKey = person.publicKey.publicKeyPem,
keyId = person.publicKey.id,
following = person.following,
followers = person.followers,
sharedInbox = person.endpoints["sharedInbox"],
locked = person.manuallyApprovesFollowers
),
idOverride
)
}
private fun entityToPerson(
actorEntity: Actor,
id: String,
) = Person(
type = emptyList(),
name = actorEntity.name,
id = id,
preferredUsername = actorEntity.name,
summary = actorEntity.description,
inbox = "$id/inbox",
outbox = "$id/outbox",
url = id,
icon = Image(
type = emptyList(),
mediaType = "image/jpeg",
url = "$id/icon.jpg"
),
publicKey = Key(
id = actorEntity.keyId,
owner = id,
publicKeyPem = actorEntity.publicKey
),
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox"),
followers = actorEntity.followers,
following = actorEntity.following,
manuallyApprovesFollowers = actorEntity.locked
)
}

View File

@ -1,44 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.activitypub.service.webfinger
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.resource.UserNotFoundException
import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import org.springframework.stereotype.Service
@Service
interface WebFingerApiService {
suspend fun findByNameAndDomain(name: String, domain: String): Actor
}
@Service
class WebFingerApiServiceImpl(
private val transaction: Transaction,
private val actorRepository: ActorRepository
) :
WebFingerApiService {
override suspend fun findByNameAndDomain(name: String, domain: String): Actor {
return transaction.transaction {
actorRepository.findByNameAndDomain(name, domain) ?: throw UserNotFoundException.withNameAndDomain(
name,
domain
)
}
}
}

View File

@ -26,7 +26,6 @@ import com.nimbusds.jose.proc.SecurityContext
import dev.usbharu.hideout.activitypub.domain.model.StringORObjectSerializer
import dev.usbharu.hideout.activitypub.domain.model.StringOrObject
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureHeaderChecker
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.event.relationship
import dev.usbharu.hideout.core.domain.model.relationship.Relationship2
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEvent
import dev.usbharu.hideout.core.domain.shared.domainevent.DomainEventBody
class RelationshipEventFactory(private val relationship2: Relationship2) {
fun createEvent(relationshipEvent: RelationshipEvent): DomainEvent {
return DomainEvent.create(relationshipEvent.eventName, RelationshipEventBody(relationship2))
}
}
class RelationshipEventBody(relationship: Relationship2) : DomainEventBody(mapOf("relationship" to relationship))
enum class RelationshipEvent(val eventName: String) {
follow("RelationshipFollow"),
unfollow("RelationshipUnfollow"),
block("RelationshipBlock"),
unblock("RelationshipUnblock"),
mute("RelationshipMute"),
unmute("RelationshipUnmute"),
followRequest("RelationshipFollowRequest"),
unfollowRequest("RelationshipUnfollowRequest"),
}

View File

@ -1,19 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.actor
data class Acct(val username: String, val domain: String? = null, val isRemote: Boolean = domain == null)

View File

@ -1,269 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.actor
import dev.usbharu.hideout.application.config.ApplicationConfig
import dev.usbharu.hideout.application.config.CharacterLimit
import jakarta.validation.Validator
import jakarta.validation.constraints.*
import org.hibernate.validator.constraints.URL
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.Instant
import kotlin.math.max
@Deprecated("Actor2を使う")
data class Actor private constructor(
@get:NotNull
@get:Positive
val id: Long,
@get:Pattern(regexp = "^[a-zA-Z0-9_-]{1,300}\$")
@get:Size(min = 1)
val name: String,
val domain: String,
val screenName: String,
val description: String,
@get:URL
val inbox: String,
@get:URL
val outbox: String,
@get:URL
val url: String,
@get:NotBlank
val publicKey: String,
val privateKey: String? = null,
@get:PastOrPresent
val createdAt: Instant,
@get:NotBlank
val keyId: String,
val followers: String? = null,
val following: String? = null,
@get:PositiveOrZero
val instance: Long,
val locked: Boolean,
val followersCount: Int = 0,
val followingCount: Int = 0,
val postsCount: Int = 0,
val lastPostDate: Instant? = null,
val emojis: List<Long> = emptyList(),
) {
@Component
class UserBuilder(
private val characterLimit: CharacterLimit,
private val applicationConfig: ApplicationConfig,
private val validator: Validator,
) {
private val logger = LoggerFactory.getLogger(UserBuilder::class.java)
@Suppress("LongParameterList", "FunctionMinLength", "LongMethod")
fun of(
id: Long,
name: String,
domain: String,
screenName: String,
description: String,
inbox: String,
outbox: String,
url: String,
publicKey: String,
privateKey: String? = null,
createdAt: Instant,
keyId: String,
following: String? = null,
followers: String? = null,
instance: Long,
locked: Boolean,
followersCount: Int = 0,
followingCount: Int = 0,
postsCount: Int = 0,
lastPostDate: Instant? = null,
emojis: List<Long> = emptyList(),
): Actor {
if (id == 0L) {
return Actor(
id = id,
name = name,
domain = domain,
screenName = screenName,
description = description,
inbox = inbox,
outbox = outbox,
url = url,
publicKey = publicKey,
privateKey = privateKey,
createdAt = createdAt,
keyId = keyId,
followers = followers,
following = following,
instance = instance,
locked = locked,
followersCount = followersCount,
followingCount = followingCount,
postsCount = postsCount,
lastPostDate = lastPostDate,
emojis = emojis
)
}
// idは0未満ではいけない
require(id >= 0) { "id must be greater than or equal to 0." }
// nameは空文字以外を含める必要がある
require(name.isNotBlank()) { "name must contain non-blank characters." }
// nameは指定された長さ以下である必要がある
val limitedName = if (name.length >= characterLimit.account.id) {
logger.warn("name must not exceed ${characterLimit.account.id} characters.")
name.substring(0, characterLimit.account.id)
} else {
name
}
// domainは空文字以外を含める必要がある
require(domain.isNotBlank()) { "domain must contain non-blank characters." }
// domainは指定された長さ以下である必要がある
require(domain.length <= characterLimit.general.domain) {
"domain must not exceed ${characterLimit.general.domain} characters."
}
// screenNameは空文字以外を含める必要がある
require(screenName.isNotBlank()) { "screenName must contain non-blank characters." }
// screenNameは指定された長さ以下である必要がある
val limitedScreenName = if (screenName.length >= characterLimit.account.name) {
logger.warn("screenName must not exceed ${characterLimit.account.name} characters.")
screenName.substring(0, characterLimit.account.name)
} else {
screenName
}
// descriptionは指定された長さ以下である必要がある
val limitedDescription = if (description.length >= characterLimit.account.description) {
logger.warn("description must not exceed ${characterLimit.account.description} characters.")
description.substring(0, characterLimit.account.description)
} else {
description
}
// ローカルユーザーはpasswordとprivateKeyをnullにしてはいけない
if (domain == applicationConfig.url.host) {
requireNotNull(privateKey) { "password and privateKey must not be null for local users." }
}
// urlは空文字以外を含める必要がある
require(url.isNotBlank()) { "url must contain non-blank characters." }
// urlは指定された長さ以下である必要がある
require(url.length <= characterLimit.general.url) {
"url must not exceed ${characterLimit.general.url} characters."
}
// inboxは空文字以外を含める必要がある
require(inbox.isNotBlank()) { "inbox must contain non-blank characters." }
// inboxは指定された長さ以下である必要がある
require(inbox.length <= characterLimit.general.url) {
"inbox must not exceed ${characterLimit.general.url} characters."
}
// outboxは空文字以外を含める必要がある
require(outbox.isNotBlank()) { "outbox must contain non-blank characters." }
// outboxは指定された長さ以下である必要がある
require(outbox.length <= characterLimit.general.url) {
"outbox must not exceed ${characterLimit.general.url} characters."
}
require(keyId.isNotBlank()) {
"keyId must contain non-blank characters."
}
val actor = Actor(
id = id,
name = limitedName,
domain = domain,
screenName = limitedScreenName,
description = limitedDescription,
inbox = inbox,
outbox = outbox,
url = url,
publicKey = publicKey,
privateKey = privateKey,
createdAt = createdAt,
keyId = keyId,
followers = followers,
following = following,
instance = instance,
locked = locked,
followersCount = max(0, followersCount),
followingCount = max(0, followingCount),
postsCount = max(0, postsCount),
lastPostDate = lastPostDate,
emojis = emojis
)
val validate = validator.validate(actor)
for (constraintViolation in validate) {
throw IllegalArgumentException("${constraintViolation.propertyPath} : ${constraintViolation.message}")
}
return actor
}
}
fun incrementFollowing(): Actor = this.copy(followingCount = this.followingCount + 1)
fun decrementFollowing(): Actor = this.copy(followingCount = this.followingCount - 1)
fun incrementFollowers(): Actor = this.copy(followersCount = this.followersCount + 1)
fun decrementFollowers(): Actor = this.copy(followersCount = this.followersCount - 1)
fun incrementPostsCount(): Actor = this.copy(postsCount = this.postsCount + 1)
fun decrementPostsCount(): Actor = this.copy(postsCount = this.postsCount - 1)
fun withLastPostAt(lastPostDate: Instant): Actor = this.copy(lastPostDate = lastPostDate)
override fun toString(): String {
return "Actor(" +
"id=$id, " +
"name='$name', " +
"domain='$domain', " +
"screenName='$screenName', " +
"description='$description', " +
"inbox='$inbox', " +
"outbox='$outbox', " +
"url='$url', " +
"publicKey='$publicKey', " +
"privateKey=$privateKey, " +
"createdAt=$createdAt, " +
"keyId='$keyId', " +
"followers=$followers, " +
"following=$following, " +
"instance=$instance, " +
"locked=$locked, " +
"followersCount=$followersCount, " +
"followingCount=$followingCount, " +
"postsCount=$postsCount, " +
"lastPostDate=$lastPostDate, " +
"emojis=$emojis" +
")"
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2024 usbharu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.usbharu.hideout.core.domain.model.actor
import org.springframework.stereotype.Repository
@Repository
@Suppress("TooManyFunctions")
interface ActorRepository {
suspend fun save(actor: Actor): Actor
suspend fun findById(id: Long): Actor?
suspend fun findByIdWithLock(id: Long): Actor?
suspend fun findAll(limit: Int, offset: Long): List<Actor>
suspend fun findByName(name: String): List<Actor>
suspend fun findByNameAndDomain(name: String, domain: String): Actor?
suspend fun findByNameAndDomainWithLock(name: String, domain: String): Actor?
suspend fun findByUrl(url: String): Actor?
suspend fun findByUrlWithLock(url: String): Actor?
suspend fun findByIds(ids: List<Long>): List<Actor>
suspend fun findByKeyId(keyId: String): Actor?
suspend fun delete(id: Long)
suspend fun nextId(): Long
}

Some files were not shown because too many files have changed in this diff Show More