라이브 방 - 아고라 설정 및 라이브 방 관련 API
This commit is contained in:
parent
f393c7630e
commit
58a7f87ffd
|
@ -0,0 +1,171 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.TreeMap
|
||||||
|
|
||||||
|
class AccessToken(
|
||||||
|
var appId: String,
|
||||||
|
private val appCertificate: String,
|
||||||
|
val channelName: String,
|
||||||
|
private val uid: String,
|
||||||
|
var crcChannelName: Int = 0,
|
||||||
|
private var crcUid: Int = 0,
|
||||||
|
val message: PrivilegeMessage = PrivilegeMessage()
|
||||||
|
) {
|
||||||
|
|
||||||
|
private lateinit var signature: ByteArray
|
||||||
|
private lateinit var messageRawContent: ByteArray
|
||||||
|
|
||||||
|
enum class Privileges(value: Int) {
|
||||||
|
JoinChannel(1),
|
||||||
|
PublishAudioStream(2),
|
||||||
|
PublishVideoStream(3),
|
||||||
|
PublishDataStream(4), // For RTM only
|
||||||
|
RtmLogin(1000);
|
||||||
|
|
||||||
|
var intValue: Short
|
||||||
|
|
||||||
|
init {
|
||||||
|
intValue = value.toShort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun build(): String {
|
||||||
|
if (!AgoraUtils.isUUID(appId)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (!AgoraUtils.isUUID(appCertificate)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
messageRawContent = AgoraUtils.pack(message)
|
||||||
|
signature = generateSignature(
|
||||||
|
appCertificate,
|
||||||
|
appId,
|
||||||
|
channelName,
|
||||||
|
uid,
|
||||||
|
messageRawContent
|
||||||
|
)
|
||||||
|
crcChannelName = AgoraUtils.crc32(channelName)
|
||||||
|
crcUid = AgoraUtils.crc32(uid)
|
||||||
|
val packContent = PackContent(signature, crcChannelName, crcUid, messageRawContent)
|
||||||
|
val content: ByteArray = AgoraUtils.pack(packContent)
|
||||||
|
return getVersion() + appId + AgoraUtils.base64Encode(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addPrivilege(privilege: Privileges, expireTimestamp: Int) {
|
||||||
|
message.messages[privilege.intValue] = expireTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVersion(): String {
|
||||||
|
return VER
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(java.lang.Exception::class)
|
||||||
|
fun generateSignature(
|
||||||
|
appCertificate: String,
|
||||||
|
appID: String,
|
||||||
|
channelName: String,
|
||||||
|
uid: String,
|
||||||
|
message: ByteArray
|
||||||
|
): ByteArray {
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
try {
|
||||||
|
baos.write(appID.toByteArray())
|
||||||
|
baos.write(channelName.toByteArray())
|
||||||
|
baos.write(uid.toByteArray())
|
||||||
|
baos.write(message)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AgoraUtils.hmacSign(appCertificate, baos.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromString(token: String): Boolean {
|
||||||
|
if (getVersion() != token.substring(0, AgoraUtils.VERSION_LENGTH)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
appId = token.substring(AgoraUtils.VERSION_LENGTH, AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH)
|
||||||
|
val packContent = PackContent()
|
||||||
|
AgoraUtils.unpack(
|
||||||
|
AgoraUtils.base64Decode(
|
||||||
|
token.substring(
|
||||||
|
AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH,
|
||||||
|
token.length
|
||||||
|
)
|
||||||
|
),
|
||||||
|
packContent
|
||||||
|
)
|
||||||
|
signature = packContent.signature
|
||||||
|
crcChannelName = packContent.crcChannelName
|
||||||
|
crcUid = packContent.crcUid
|
||||||
|
messageRawContent = packContent.rawMessage
|
||||||
|
AgoraUtils.unpack(messageRawContent, message)
|
||||||
|
} catch (e: java.lang.Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrivilegeMessage : PackableEx {
|
||||||
|
var salt: Int
|
||||||
|
var ts: Int
|
||||||
|
var messages: TreeMap<Short, Int>
|
||||||
|
|
||||||
|
override fun marshal(out: ByteBuf): ByteBuf {
|
||||||
|
return out.put(salt).put(ts).putIntMap(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unmarshal(input: ByteBuf) {
|
||||||
|
salt = input.readInt()
|
||||||
|
ts = input.readInt()
|
||||||
|
messages = input.readIntMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
salt = AgoraUtils.randomInt()
|
||||||
|
ts = AgoraUtils.getTimestamp() + 24 * 3600
|
||||||
|
messages = TreeMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PackContent() : PackableEx {
|
||||||
|
var signature: ByteArray = byteArrayOf()
|
||||||
|
var crcChannelName = 0
|
||||||
|
var crcUid = 0
|
||||||
|
var rawMessage: ByteArray = byteArrayOf()
|
||||||
|
|
||||||
|
constructor(signature: ByteArray, crcChannelName: Int, crcUid: Int, rawMessage: ByteArray) : this() {
|
||||||
|
this.signature = signature
|
||||||
|
this.crcChannelName = crcChannelName
|
||||||
|
this.crcUid = crcUid
|
||||||
|
this.rawMessage = rawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun marshal(out: ByteBuf): ByteBuf {
|
||||||
|
return out
|
||||||
|
.put(signature)
|
||||||
|
.put(crcChannelName)
|
||||||
|
.put(crcUid).put(rawMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unmarshal(input: ByteBuf) {
|
||||||
|
signature = input.readBytes()
|
||||||
|
crcChannelName = input.readInt()
|
||||||
|
crcUid = input.readInt()
|
||||||
|
rawMessage = input.readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val VER = "006"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.zip.CRC32
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object AgoraUtils {
|
||||||
|
const val HMAC_SHA256_LENGTH: Long = 32
|
||||||
|
const val VERSION_LENGTH = 3
|
||||||
|
const val APP_ID_LENGTH = 32
|
||||||
|
|
||||||
|
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class)
|
||||||
|
fun hmacSign(keyString: String, msg: ByteArray?): ByteArray {
|
||||||
|
val keySpec = SecretKeySpec(keyString.toByteArray(), "HmacSHA256")
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(keySpec)
|
||||||
|
return mac.doFinal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pack(packableEx: PackableEx): ByteArray {
|
||||||
|
val buffer = ByteBuf()
|
||||||
|
packableEx.marshal(buffer)
|
||||||
|
return buffer.asBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpack(data: ByteArray?, packableEx: PackableEx) {
|
||||||
|
val buffer = ByteBuf(data!!)
|
||||||
|
packableEx.unmarshal(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun base64Encode(data: ByteArray?): String {
|
||||||
|
val encodedBytes: ByteArray = Base64.encodeBase64(data)
|
||||||
|
return String(encodedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun base64Decode(data: String): ByteArray {
|
||||||
|
return Base64.decodeBase64(data.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun crc32(data: String): Int {
|
||||||
|
// get bytes from string
|
||||||
|
val bytes = data.toByteArray()
|
||||||
|
return crc32(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun crc32(bytes: ByteArray?): Int {
|
||||||
|
val checksum = CRC32()
|
||||||
|
checksum.update(bytes)
|
||||||
|
return checksum.value.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTimestamp(): Int {
|
||||||
|
return (Date().time / 1000).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomInt(): Int {
|
||||||
|
return SecureRandom().nextInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isUUID(uuid: String): Boolean {
|
||||||
|
return if (uuid.length != 32) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
uuid.matches("\\p{XDigit}+".toRegex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.util.TreeMap
|
||||||
|
|
||||||
|
class ByteBuf() {
|
||||||
|
private var buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
constructor(bytes: ByteArray) : this() {
|
||||||
|
this.buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun asBytes(): ByteArray {
|
||||||
|
val out = ByteArray(buffer.position())
|
||||||
|
buffer.rewind()
|
||||||
|
buffer[out, 0, out.size]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// packUint16
|
||||||
|
fun put(v: Short): ByteBuf {
|
||||||
|
buffer.putShort(v)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(v: ByteArray): ByteBuf {
|
||||||
|
put(v.size.toShort())
|
||||||
|
buffer.put(v)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// packUint32
|
||||||
|
fun put(v: Int): ByteBuf {
|
||||||
|
buffer.putInt(v)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(v: Long): ByteBuf {
|
||||||
|
buffer.putLong(v)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(v: String): ByteBuf {
|
||||||
|
return put(v.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(extra: TreeMap<Short, String>): ByteBuf {
|
||||||
|
put(extra.size.toShort())
|
||||||
|
for ((key, value) in extra.entries) {
|
||||||
|
put(key)
|
||||||
|
put(value)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putIntMap(extra: TreeMap<Short, Int>): ByteBuf {
|
||||||
|
put(extra.size.toShort())
|
||||||
|
for ((key, value) in extra.entries) {
|
||||||
|
put(key)
|
||||||
|
put(value)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readShort(): Short {
|
||||||
|
return buffer.short
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt(): Int {
|
||||||
|
return buffer.int
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBytes(): ByteArray {
|
||||||
|
val length = readShort()
|
||||||
|
val bytes = ByteArray(length.toInt())
|
||||||
|
buffer[bytes]
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readString(): String {
|
||||||
|
val bytes = readBytes()
|
||||||
|
return String(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readMap(): TreeMap<Short, String> {
|
||||||
|
val map = TreeMap<Short, String>()
|
||||||
|
val length = readShort()
|
||||||
|
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val k = readShort()
|
||||||
|
val v = readString()
|
||||||
|
map[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readIntMap(): TreeMap<Short, Int> {
|
||||||
|
val map = TreeMap<Short, Int>()
|
||||||
|
val length = readShort()
|
||||||
|
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val k = readShort()
|
||||||
|
val v = readInt()
|
||||||
|
map[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,256 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
|
import java.util.TreeMap
|
||||||
|
|
||||||
|
class DynamicKey5 {
|
||||||
|
lateinit var content: DynamicKey5Content
|
||||||
|
|
||||||
|
fun fromString(key: String): Boolean {
|
||||||
|
if (key.substring(0, 3) != version) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val rawContent: ByteArray = Base64().decode(key.substring(3))
|
||||||
|
if (rawContent.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
content = DynamicKey5Content()
|
||||||
|
val buffer = ByteBuf(rawContent)
|
||||||
|
content.unmarshall(buffer)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val version = "005"
|
||||||
|
const val noUpload = "0"
|
||||||
|
const val audioVideoUpload = "3"
|
||||||
|
|
||||||
|
// ServiceType
|
||||||
|
const val MEDIA_CHANNEL_SERVICE: Short = 1
|
||||||
|
const val RECORDING_SERVICE: Short = 2
|
||||||
|
const val PUBLIC_SHARING_SERVICE: Short = 3
|
||||||
|
const val IN_CHANNEL_PERMISSION: Short = 4
|
||||||
|
|
||||||
|
// InChannelPermissionKey
|
||||||
|
const val ALLOW_UPLOAD_IN_CHANNEL: Short = 1
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun generateSignature(
|
||||||
|
appCertificate: String,
|
||||||
|
service: Short,
|
||||||
|
appID: String,
|
||||||
|
unixTs: Int,
|
||||||
|
salt: Int,
|
||||||
|
channelName: String,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int,
|
||||||
|
extra: TreeMap<Short, String>
|
||||||
|
): String {
|
||||||
|
// decode hex to avoid case problem
|
||||||
|
val hex = Hex()
|
||||||
|
val rawAppID: ByteArray = hex.decode(appID.toByteArray())
|
||||||
|
val rawAppCertificate: ByteArray = hex.decode(appCertificate.toByteArray())
|
||||||
|
val m = Message(
|
||||||
|
service,
|
||||||
|
rawAppID,
|
||||||
|
unixTs,
|
||||||
|
salt,
|
||||||
|
channelName,
|
||||||
|
(uid and 0xFFFFFFFFL).toInt(),
|
||||||
|
expiredTs,
|
||||||
|
extra
|
||||||
|
)
|
||||||
|
val toSign: ByteArray = pack(m)
|
||||||
|
return String(Hex.encodeHex(DynamicKeyUtil.encodeHMAC(rawAppCertificate, toSign), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(java.lang.Exception::class)
|
||||||
|
fun generateDynamicKey(
|
||||||
|
appID: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channel: String,
|
||||||
|
ts: Int,
|
||||||
|
salt: Int,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int,
|
||||||
|
extra: TreeMap<Short, String>,
|
||||||
|
service: Short
|
||||||
|
): String {
|
||||||
|
val signature = generateSignature(appCertificate, service, appID, ts, salt, channel, uid, expiredTs, extra)
|
||||||
|
val content =
|
||||||
|
DynamicKey5Content(service, signature, Hex().decode(appID.toByteArray()), ts, salt, expiredTs, extra)
|
||||||
|
val bytes: ByteArray = pack(content)
|
||||||
|
val encoded = Base64().encode(bytes)
|
||||||
|
val base64 = String(encoded)
|
||||||
|
return version + base64
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pack(content: Packable): ByteArray {
|
||||||
|
val buffer = ByteBuf()
|
||||||
|
content.marshal(buffer)
|
||||||
|
return buffer.asBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun generatePublicSharingKey(
|
||||||
|
appID: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channel: String,
|
||||||
|
ts: Int,
|
||||||
|
salt: Int,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int
|
||||||
|
) = generateDynamicKey(
|
||||||
|
appID,
|
||||||
|
appCertificate,
|
||||||
|
channel,
|
||||||
|
ts,
|
||||||
|
salt,
|
||||||
|
uid,
|
||||||
|
expiredTs,
|
||||||
|
TreeMap(),
|
||||||
|
PUBLIC_SHARING_SERVICE
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun generateRecordingKey(
|
||||||
|
appID: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channel: String,
|
||||||
|
ts: Int,
|
||||||
|
salt: Int,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int
|
||||||
|
) = generateDynamicKey(
|
||||||
|
appID,
|
||||||
|
appCertificate,
|
||||||
|
channel,
|
||||||
|
ts,
|
||||||
|
salt,
|
||||||
|
uid,
|
||||||
|
expiredTs,
|
||||||
|
TreeMap(),
|
||||||
|
RECORDING_SERVICE
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun generateMediaChannelKey(
|
||||||
|
appID: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channel: String,
|
||||||
|
ts: Int,
|
||||||
|
salt: Int,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int
|
||||||
|
) = generateDynamicKey(
|
||||||
|
appID,
|
||||||
|
appCertificate,
|
||||||
|
channel,
|
||||||
|
ts,
|
||||||
|
salt,
|
||||||
|
uid,
|
||||||
|
expiredTs,
|
||||||
|
TreeMap(),
|
||||||
|
MEDIA_CHANNEL_SERVICE
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun generateInChannelPermissionKey(
|
||||||
|
appID: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channel: String,
|
||||||
|
ts: Int,
|
||||||
|
salt: Int,
|
||||||
|
uid: Long,
|
||||||
|
expiredTs: Int,
|
||||||
|
permission: String
|
||||||
|
): String {
|
||||||
|
val extra = TreeMap<Short, String>()
|
||||||
|
extra[ALLOW_UPLOAD_IN_CHANNEL] = permission
|
||||||
|
return generateDynamicKey(
|
||||||
|
appID,
|
||||||
|
appCertificate,
|
||||||
|
channel,
|
||||||
|
ts,
|
||||||
|
salt,
|
||||||
|
uid,
|
||||||
|
expiredTs,
|
||||||
|
extra,
|
||||||
|
IN_CHANNEL_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Message(
|
||||||
|
var serviceType: Short,
|
||||||
|
var appID: ByteArray,
|
||||||
|
var unixTs: Int,
|
||||||
|
var salt: Int,
|
||||||
|
var channelName: String,
|
||||||
|
var uid: Int,
|
||||||
|
var expiredTs: Int,
|
||||||
|
var extra: TreeMap<Short, String>
|
||||||
|
) : Packable {
|
||||||
|
override fun marshal(out: ByteBuf): ByteBuf {
|
||||||
|
return out
|
||||||
|
.put(serviceType)
|
||||||
|
.put(appID)
|
||||||
|
.put(unixTs)
|
||||||
|
.put(salt)
|
||||||
|
.put(channelName)
|
||||||
|
.put(uid)
|
||||||
|
.put(expiredTs)
|
||||||
|
.put(extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicKey5Content() : Packable {
|
||||||
|
var serviceType: Short = 0
|
||||||
|
var signature: String? = null
|
||||||
|
var appID: ByteArray = byteArrayOf()
|
||||||
|
var unixTs = 0
|
||||||
|
var salt = 0
|
||||||
|
var expiredTs = 0
|
||||||
|
var extra: TreeMap<Short, String>? = null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
serviceType: Short,
|
||||||
|
signature: String?,
|
||||||
|
appID: ByteArray,
|
||||||
|
unixTs: Int,
|
||||||
|
salt: Int,
|
||||||
|
expiredTs: Int,
|
||||||
|
extra: TreeMap<Short, String>
|
||||||
|
) : this() {
|
||||||
|
this.serviceType = serviceType
|
||||||
|
this.signature = signature
|
||||||
|
this.appID = appID
|
||||||
|
this.unixTs = unixTs
|
||||||
|
this.salt = salt
|
||||||
|
this.expiredTs = expiredTs
|
||||||
|
this.extra = extra
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun marshal(out: ByteBuf): ByteBuf {
|
||||||
|
return out
|
||||||
|
.put(serviceType)
|
||||||
|
.put(signature!!)
|
||||||
|
.put(appID)
|
||||||
|
.put(unixTs)
|
||||||
|
.put(salt)
|
||||||
|
.put(expiredTs)
|
||||||
|
.put(extra!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unmarshall(input: ByteBuf) {
|
||||||
|
serviceType = input.readShort()
|
||||||
|
signature = input.readString()
|
||||||
|
appID = input.readBytes()
|
||||||
|
unixTs = input.readInt()
|
||||||
|
salt = input.readInt()
|
||||||
|
expiredTs = input.readInt()
|
||||||
|
extra = input.readMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by hefeng on 15/8/10.
|
||||||
|
* Util to generate Agora media dynamic key.
|
||||||
|
*/
|
||||||
|
object DynamicKeyUtil {
|
||||||
|
@Throws(NoSuchAlgorithmException::class, InvalidKeyException::class)
|
||||||
|
fun encodeHMAC(key: String, message: ByteArray?): ByteArray? {
|
||||||
|
return encodeHMAC(key.toByteArray(), message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, InvalidKeyException::class)
|
||||||
|
fun encodeHMAC(key: ByteArray?, message: ByteArray?): ByteArray? {
|
||||||
|
val keySpec = SecretKeySpec(key, "HmacSHA1")
|
||||||
|
val mac = Mac.getInstance("HmacSHA1")
|
||||||
|
mac.init(keySpec)
|
||||||
|
return mac.doFinal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bytesToHex(`in`: ByteArray): String {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
for (b in `in`) {
|
||||||
|
builder.append(String.format("%02x", b))
|
||||||
|
}
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
interface Packable {
|
||||||
|
fun marshal(out: ByteBuf): ByteBuf
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
interface PackableEx : Packable {
|
||||||
|
fun unmarshal(input: ByteBuf)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class RtcTokenBuilder {
|
||||||
|
/**
|
||||||
|
* Builds an RTC token using an int uid.
|
||||||
|
*
|
||||||
|
* @param appId The App ID issued to you by Agora.
|
||||||
|
* @param appCertificate Certificate of the application that you registered in
|
||||||
|
* the Agora Dashboard.
|
||||||
|
* @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are:
|
||||||
|
*
|
||||||
|
* * The 26 lowercase English letters: a to z.
|
||||||
|
* * The 26 uppercase English letters: A to Z.
|
||||||
|
* * The 10 digits: 0 to 9.
|
||||||
|
* * The space.
|
||||||
|
* * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",".
|
||||||
|
*
|
||||||
|
* @param uid User ID. A 32-bit unsigned integer with a value ranging from
|
||||||
|
* 1 to (2^32-1).
|
||||||
|
* @param role The user role.
|
||||||
|
*
|
||||||
|
* * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
|
||||||
|
* * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
|
||||||
|
*
|
||||||
|
* @param privilegeTs Represented by the number of seconds elapsed since 1/1/1970.
|
||||||
|
* If, for example, you want to access the Agora Service within 10 minutes
|
||||||
|
* after the token is generated, set expireTimestamp as the current time stamp
|
||||||
|
* + 600 (seconds).
|
||||||
|
*/
|
||||||
|
fun buildTokenWithUid(
|
||||||
|
appId: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channelName: String,
|
||||||
|
uid: Int,
|
||||||
|
privilegeTs: Int
|
||||||
|
): String {
|
||||||
|
val account = if (uid == 0) "" else uid.toString()
|
||||||
|
return buildTokenWithUserAccount(
|
||||||
|
appId,
|
||||||
|
appCertificate,
|
||||||
|
channelName,
|
||||||
|
account,
|
||||||
|
privilegeTs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an RTC token using a string userAccount.
|
||||||
|
*
|
||||||
|
* @param appId The App ID issued to you by Agora.
|
||||||
|
* @param appCertificate Certificate of the application that you registered in
|
||||||
|
* the Agora Dashboard.
|
||||||
|
* @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are:
|
||||||
|
*
|
||||||
|
* * The 26 lowercase English letters: a to z.
|
||||||
|
* * The 26 uppercase English letters: A to Z.
|
||||||
|
* * The 10 digits: 0 to 9.
|
||||||
|
* * The space.
|
||||||
|
* * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",".
|
||||||
|
*
|
||||||
|
* @param account The user account.
|
||||||
|
* @param role The user role.
|
||||||
|
*
|
||||||
|
* * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
|
||||||
|
* * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
|
||||||
|
*
|
||||||
|
* @param privilegeTs represented by the number of seconds elapsed since 1/1/1970.
|
||||||
|
* If, for example, you want to access the Agora Service within 10 minutes
|
||||||
|
* after the token is generated, set expireTimestamp as the current time stamp
|
||||||
|
* + 600 (seconds).
|
||||||
|
*/
|
||||||
|
fun buildTokenWithUserAccount(
|
||||||
|
appId: String,
|
||||||
|
appCertificate: String,
|
||||||
|
channelName: String,
|
||||||
|
account: String,
|
||||||
|
privilegeTs: Int
|
||||||
|
): String {
|
||||||
|
// Assign appropriate access privileges to each role.
|
||||||
|
val builder = AccessToken(appId, appCertificate, channelName, account)
|
||||||
|
builder.addPrivilege(AccessToken.Privileges.JoinChannel, privilegeTs)
|
||||||
|
builder.addPrivilege(AccessToken.Privileges.PublishAudioStream, privilegeTs)
|
||||||
|
builder.addPrivilege(AccessToken.Privileges.PublishVideoStream, privilegeTs)
|
||||||
|
builder.addPrivilege(AccessToken.Privileges.PublishDataStream, privilegeTs)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
builder.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package kr.co.vividnext.sodalive.agora
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.agora.AccessToken.Privileges
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class RtmTokenBuilder {
|
||||||
|
|
||||||
|
lateinit var mTokenCreator: AccessToken
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun buildToken(
|
||||||
|
appId: String,
|
||||||
|
appCertificate: String,
|
||||||
|
uid: String,
|
||||||
|
privilegeTs: Int
|
||||||
|
): String {
|
||||||
|
mTokenCreator = AccessToken(appId, appCertificate, uid, "")
|
||||||
|
mTokenCreator.addPrivilege(Privileges.RtmLogin, privilegeTs)
|
||||||
|
return mTokenCreator.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPrivilege(privilege: Privileges?, expireTs: Int) {
|
||||||
|
mTokenCreator.addPrivilege(privilege!!, expireTs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initTokenBuilder(originToken: String?): Boolean {
|
||||||
|
mTokenCreator.fromString(originToken!!)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ interface CanQueryRepository {
|
||||||
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
|
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
|
||||||
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
|
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
|
||||||
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
|
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
|
||||||
fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan?
|
fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage = CanUsage.LIVE): UseCan?
|
||||||
}
|
}
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
@ -113,7 +113,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
|
||||||
.fetchFirst()
|
.fetchFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? {
|
override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage): UseCan? {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.selectFrom(useCan)
|
.selectFrom(useCan)
|
||||||
.innerJoin(useCan.member, member)
|
.innerJoin(useCan.member, member)
|
||||||
|
@ -121,7 +121,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
|
||||||
.where(
|
.where(
|
||||||
member.id.eq(memberId)
|
member.id.eq(memberId)
|
||||||
.and(liveRoom.id.eq(roomId))
|
.and(liveRoom.id.eq(roomId))
|
||||||
.and(useCan.canUsage.eq(CanUsage.LIVE))
|
.and(useCan.canUsage.eq(canUsage))
|
||||||
.and(useCan.isRefund.isFalse)
|
.and(useCan.isRefund.isFalse)
|
||||||
)
|
)
|
||||||
.orderBy(useCan.id.desc())
|
.orderBy(useCan.id.desc())
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
|
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||||
|
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||||
|
import kr.co.vividnext.sodalive.member.QMember
|
||||||
|
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class ExplorerQueryRepository(
|
||||||
|
private val queryFactory: JPAQueryFactory,
|
||||||
|
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) {
|
||||||
|
fun getNotificationUserIds(creatorId: Long): List<Long> {
|
||||||
|
return queryFactory
|
||||||
|
.select(creatorFollowing.member.id)
|
||||||
|
.from(creatorFollowing)
|
||||||
|
.where(
|
||||||
|
creatorFollowing.isActive.isTrue
|
||||||
|
.and(creatorFollowing.creator.id.eq(creatorId))
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberDonationRanking(
|
||||||
|
creatorId: Long,
|
||||||
|
limit: Long,
|
||||||
|
offset: Long = 0,
|
||||||
|
withDonationCoin: Boolean
|
||||||
|
): List<MemberDonationRankingResponse> {
|
||||||
|
val creator = QMember("creator")
|
||||||
|
val member = QMember("user")
|
||||||
|
|
||||||
|
val donation = useCan.rewardCan.add(useCan.can).sum()
|
||||||
|
return queryFactory
|
||||||
|
.select(member, donation)
|
||||||
|
.from(useCan)
|
||||||
|
.join(useCan.room, liveRoom)
|
||||||
|
.join(liveRoom.member, creator)
|
||||||
|
.join(useCan.member, member)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.where(
|
||||||
|
useCan.canUsage.eq(CanUsage.DONATION)
|
||||||
|
.and(useCan.isRefund.isFalse)
|
||||||
|
.and(creator.id.eq(creatorId))
|
||||||
|
)
|
||||||
|
.groupBy(useCan.member.id)
|
||||||
|
.orderBy(donation.desc(), member.id.desc())
|
||||||
|
.fetch()
|
||||||
|
.map {
|
||||||
|
val account = it.get(member)!!
|
||||||
|
val donationCoin = it.get(donation)!!
|
||||||
|
MemberDonationRankingResponse(
|
||||||
|
account.id!!,
|
||||||
|
account.nickname,
|
||||||
|
if (account.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${account.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
},
|
||||||
|
if (withDonationCoin) donationCoin else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
data class MemberDonationRankingResponse(
|
||||||
|
val userId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileImage: String,
|
||||||
|
val donationCoin: Int
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
|
data class GetLiveRoomUserProfileResponse(
|
||||||
|
val userId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileUrl: String,
|
||||||
|
val gender: String,
|
||||||
|
val instagramUrl: String,
|
||||||
|
val youtubeUrl: String,
|
||||||
|
val websiteUrl: String,
|
||||||
|
val blogUrl: String,
|
||||||
|
val introduce: String,
|
||||||
|
val tags: String,
|
||||||
|
val isSpeaker: Boolean?,
|
||||||
|
val isManager: Boolean?,
|
||||||
|
val isFollowing: Boolean?,
|
||||||
|
val isBlock: Boolean
|
||||||
|
)
|
|
@ -3,9 +3,12 @@ package kr.co.vividnext.sodalive.live.room
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest
|
import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
@ -106,4 +109,124 @@ class LiveRoomController(private val service: LiveRoomService) {
|
||||||
|
|
||||||
ApiResponse.ok(service.editLiveRoomInfo(roomId, coverImage, requestString, member))
|
ApiResponse.ok(service.editLiveRoomInfo(roomId, coverImage, requestString, member))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/info/{id}")
|
||||||
|
fun getRoomInfo(
|
||||||
|
@PathVariable id: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getRoomInfo(roomId = id, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/donation-message")
|
||||||
|
fun getDonationMessageList(
|
||||||
|
@RequestParam roomId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getDonationMessageList(roomId, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/donation-message")
|
||||||
|
fun removeDonationMessage(
|
||||||
|
@RequestBody request: DeleteLiveRoomDonationMessage,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.deleteDonationMessage(request, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{room_id}/profile/{user_id}")
|
||||||
|
fun getUserProfile(
|
||||||
|
@PathVariable("room_id") roomId: Long,
|
||||||
|
@PathVariable("user_id") userId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getUserProfile(roomId, userId, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/donation-total")
|
||||||
|
fun donationTotal(
|
||||||
|
@PathVariable("id") roomId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getDonationTotal(roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/info/set/speaker")
|
||||||
|
fun setSpeaker(
|
||||||
|
@RequestBody request: SetManagerOrSpeakerOrAudienceRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.setSpeaker(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/info/set/listener")
|
||||||
|
fun setListener(
|
||||||
|
@RequestBody request: SetManagerOrSpeakerOrAudienceRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.setListener(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/info/set/manager")
|
||||||
|
fun setManager(
|
||||||
|
@RequestBody request: SetManagerOrSpeakerOrAudienceRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.setManager(request, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/donation")
|
||||||
|
fun donation(
|
||||||
|
@RequestBody request: LiveRoomDonationRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.donation(request, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/donation/refund/{id}")
|
||||||
|
fun refundDonation(
|
||||||
|
@PathVariable id: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.refundDonation(id, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/donation-list")
|
||||||
|
fun donationList(
|
||||||
|
@PathVariable("id") roomId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getDonationStatus(roomId, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/quit")
|
||||||
|
fun quitRoom(
|
||||||
|
@RequestParam("id") roomId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
ApiResponse.ok(service.quitRoom(roomId, member))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,12 @@ import com.querydsl.core.types.Projections
|
||||||
import com.querydsl.core.types.dsl.CaseBuilder
|
import com.querydsl.core.types.dsl.CaseBuilder
|
||||||
import com.querydsl.core.types.dsl.Expressions
|
import com.querydsl.core.types.dsl.Expressions
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
|
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||||
|
import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate
|
||||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationItem
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.QGetLiveRoomDonationItem
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.QMember
|
import kr.co.vividnext.sodalive.member.QMember
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
|
@ -34,6 +39,8 @@ interface LiveRoomQueryRepository {
|
||||||
fun getLiveRoom(id: Long): LiveRoom?
|
fun getLiveRoom(id: Long): LiveRoom?
|
||||||
fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom?
|
fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom?
|
||||||
fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse?
|
fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse?
|
||||||
|
fun getDonationTotal(roomId: Long): Int?
|
||||||
|
fun getDonationList(roomId: Long, cloudFrontHost: String): List<GetLiveRoomDonationItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository {
|
class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository {
|
||||||
|
@ -143,6 +150,45 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L
|
||||||
.fetchFirst()
|
.fetchFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDonationTotal(roomId: Long): Int? {
|
||||||
|
return queryFactory
|
||||||
|
.select(useCanCalculate.can.sum())
|
||||||
|
.from(useCanCalculate)
|
||||||
|
.innerJoin(useCanCalculate.useCan, useCan)
|
||||||
|
.innerJoin(useCan.room, liveRoom)
|
||||||
|
.where(
|
||||||
|
liveRoom.id.eq(roomId)
|
||||||
|
.and(useCan.canUsage.eq(CanUsage.DONATION))
|
||||||
|
.and(useCan.isRefund.isFalse)
|
||||||
|
)
|
||||||
|
.fetchOne()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDonationList(roomId: Long, cloudFrontHost: String): List<GetLiveRoomDonationItem> {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
QGetLiveRoomDonationItem(
|
||||||
|
member.profileImage
|
||||||
|
.coalesce("profile/default-profile.png")
|
||||||
|
.prepend("/")
|
||||||
|
.prepend(cloudFrontHost),
|
||||||
|
member.nickname,
|
||||||
|
member.id.coalesce(0),
|
||||||
|
useCan.can.sum().add(useCan.rewardCan.sum())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(useCan)
|
||||||
|
.join(useCan.member, member)
|
||||||
|
.groupBy(useCan.member)
|
||||||
|
.where(
|
||||||
|
useCan.room.id.eq(roomId)
|
||||||
|
.and(useCan.canUsage.eq(CanUsage.DONATION))
|
||||||
|
.and(useCan.isRefund.isFalse)
|
||||||
|
)
|
||||||
|
.orderBy(useCan.can.sum().add(useCan.rewardCan.sum()).desc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
private fun orderByFieldAccountId(
|
private fun orderByFieldAccountId(
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
status: LiveRoomStatus,
|
status: LiveRoomStatus,
|
||||||
|
|
|
@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.vividnext.sodalive.agora.RtcTokenBuilder
|
||||||
|
import kr.co.vividnext.sodalive.agora.RtmTokenBuilder
|
||||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||||
import kr.co.vividnext.sodalive.can.CanRepository
|
import kr.co.vividnext.sodalive.can.CanRepository
|
||||||
import kr.co.vividnext.sodalive.can.charge.Charge
|
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||||
|
@ -15,6 +17,7 @@ import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
|
||||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
||||||
import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest
|
import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest
|
||||||
|
@ -23,12 +26,22 @@ import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage
|
||||||
|
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
|
||||||
|
import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
|
||||||
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
|
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
|
||||||
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
|
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
|
||||||
|
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
|
||||||
|
import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService
|
||||||
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
|
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
|
||||||
import kr.co.vividnext.sodalive.live.tag.LiveTagRepository
|
import kr.co.vividnext.sodalive.live.tag.LiveTagRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.Gender
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
|
@ -39,6 +52,7 @@ import org.springframework.web.multipart.MultipartFile
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -46,9 +60,11 @@ class LiveRoomService(
|
||||||
private val repository: LiveRoomRepository,
|
private val repository: LiveRoomRepository,
|
||||||
private val roomInfoRepository: LiveRoomInfoRedisRepository,
|
private val roomInfoRepository: LiveRoomInfoRedisRepository,
|
||||||
private val roomCancelRepository: LiveRoomCancelRepository,
|
private val roomCancelRepository: LiveRoomCancelRepository,
|
||||||
|
private val kickOutService: LiveRoomKickOutService,
|
||||||
|
|
||||||
private val useCanCalculateRepository: UseCanCalculateRepository,
|
private val useCanCalculateRepository: UseCanCalculateRepository,
|
||||||
private val reservationRepository: LiveReservationRepository,
|
private val reservationRepository: LiveReservationRepository,
|
||||||
|
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||||
private val roomVisitService: LiveRoomVisitService,
|
private val roomVisitService: LiveRoomVisitService,
|
||||||
private val canPaymentService: CanPaymentService,
|
private val canPaymentService: CanPaymentService,
|
||||||
private val chargeRepository: ChargeRepository,
|
private val chargeRepository: ChargeRepository,
|
||||||
|
@ -58,6 +74,15 @@ class LiveRoomService(
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val s3Uploader: S3Uploader,
|
private val s3Uploader: S3Uploader,
|
||||||
|
|
||||||
|
private val rtcTokenBuilder: RtcTokenBuilder,
|
||||||
|
private val rtmTokenBuilder: RtmTokenBuilder,
|
||||||
|
|
||||||
|
@Value("\${agora.app-id}")
|
||||||
|
private val agoraAppId: String,
|
||||||
|
|
||||||
|
@Value("\${agora.app-certificate}")
|
||||||
|
private val agoraAppCertificate: String,
|
||||||
|
|
||||||
@Value("\${cloud.aws.s3.bucket}")
|
@Value("\${cloud.aws.s3.bucket}")
|
||||||
private val coverImageBucket: String,
|
private val coverImageBucket: String,
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
@ -322,6 +347,7 @@ class LiveRoomService(
|
||||||
"${dateTime.hour}_${dateTime.minute}"
|
"${dateTime.hour}_${dateTime.minute}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
fun cancelLive(request: CancelLiveRequest, member: Member) {
|
fun cancelLive(request: CancelLiveRequest, member: Member) {
|
||||||
val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!)
|
val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!)
|
||||||
?: throw SodaException("해당하는 라이브가 없습니다.")
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
@ -339,8 +365,11 @@ class LiveRoomService(
|
||||||
if (room.price > 0) {
|
if (room.price > 0) {
|
||||||
val bookerList = reservationRepository.getReservationBookerList(roomId = room.id!!)
|
val bookerList = reservationRepository.getReservationBookerList(roomId = room.id!!)
|
||||||
for (booker in bookerList) {
|
for (booker in bookerList) {
|
||||||
val useCan = canRepository.getCanUsedForLiveRoomNotRefund(memberId = booker.id!!, roomId = room.id!!)
|
val useCan = canRepository.getCanUsedForLiveRoomNotRefund(
|
||||||
?: continue
|
memberId = booker.id!!,
|
||||||
|
roomId = room.id!!,
|
||||||
|
canUsage = CanUsage.LIVE
|
||||||
|
) ?: continue
|
||||||
useCan.isRefund = true
|
useCan.isRefund = true
|
||||||
|
|
||||||
val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!)
|
val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!)
|
||||||
|
@ -373,6 +402,7 @@ class LiveRoomService(
|
||||||
reservationRepository.cancelReservation(roomId = room.id!!)
|
reservationRepository.cancelReservation(roomId = room.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
fun enterLive(request: EnterOrQuitLiveRoomRequest, member: Member) {
|
fun enterLive(request: EnterOrQuitLiveRoomRequest, member: Member) {
|
||||||
val room = repository.getLiveRoom(id = request.roomId)
|
val room = repository.getLiveRoom(id = request.roomId)
|
||||||
?: throw SodaException("해당하는 라이브가 없습니다.")
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
@ -491,4 +521,342 @@ class LiveRoomService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRoomInfo(roomId: Long, member: Member): GetRoomInfoResponse {
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val room = repository.findByIdOrNull(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val currentTimeStamp = Date().time
|
||||||
|
val expireTimestamp = (currentTimeStamp + (60 * 60 * 24 * 1000)) / 1000
|
||||||
|
|
||||||
|
val rtcToken = rtcTokenBuilder.buildTokenWithUid(
|
||||||
|
agoraAppId,
|
||||||
|
agoraAppCertificate,
|
||||||
|
room.channelName!!,
|
||||||
|
member.id!!.toInt(),
|
||||||
|
expireTimestamp.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val rtmToken = rtmTokenBuilder.buildToken(
|
||||||
|
agoraAppId,
|
||||||
|
agoraAppCertificate,
|
||||||
|
member.id!!.toString(),
|
||||||
|
expireTimestamp.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList()
|
||||||
|
val isRadioMode = tags.contains("라디오") or tags.contains("콘서트")
|
||||||
|
val isAvailableDonation = room.member!!.id!! != member.id!! &&
|
||||||
|
room.member!!.role == MemberRole.CREATOR
|
||||||
|
val isFollowingManager = explorerQueryRepository
|
||||||
|
.getNotificationUserIds(room.member!!.id!!)
|
||||||
|
.contains(member.id)
|
||||||
|
|
||||||
|
val donationRankingTop3UserIds = explorerQueryRepository
|
||||||
|
.getMemberDonationRanking(
|
||||||
|
room.member!!.id!!,
|
||||||
|
3,
|
||||||
|
withDonationCoin = false
|
||||||
|
)
|
||||||
|
.asSequence()
|
||||||
|
.map { it.userId }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
return GetRoomInfoResponse(
|
||||||
|
roomId = roomId,
|
||||||
|
title = room.title,
|
||||||
|
notice = room.notice,
|
||||||
|
coverImageUrl = if (room.bgImage != null) {
|
||||||
|
if (room.bgImage!!.startsWith("https://")) {
|
||||||
|
room.bgImage!!
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/${room.bgImage!!}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (room.coverImage!!.startsWith("https://")) {
|
||||||
|
room.coverImage!!
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/${room.coverImage!!}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channelName = room.channelName!!,
|
||||||
|
rtcToken = rtcToken,
|
||||||
|
rtmToken = rtmToken,
|
||||||
|
managerId = room.member!!.id!!,
|
||||||
|
managerNickname = room.member!!.nickname,
|
||||||
|
managerProfileUrl = if (room.member!!.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${room.member!!.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
},
|
||||||
|
isFollowingManager = isFollowingManager,
|
||||||
|
participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount,
|
||||||
|
totalAvailableParticipantsCount = room.numberOfPeople,
|
||||||
|
speakerList = roomInfo.speakerList,
|
||||||
|
listenerList = roomInfo.listenerList,
|
||||||
|
managerList = roomInfo.managerList,
|
||||||
|
donationRankingTop3UserIds = donationRankingTop3UserIds,
|
||||||
|
isRadioMode = isRadioMode,
|
||||||
|
isAvailableDonation = isAvailableDonation,
|
||||||
|
isPrivateRoom = room.type == LiveRoomType.PRIVATE,
|
||||||
|
password = room.password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDonationMessageList(roomId: Long, member: Member): List<LiveRoomDonationMessage> {
|
||||||
|
val room = repository.findByIdOrNull(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
|
||||||
|
if (member.id!! != room.member!!.id!!) {
|
||||||
|
throw SodaException("잘못된 요청입니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
return roomInfo.donationMessageList
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDonationMessage(request: DeleteLiveRoomDonationMessage, member: Member) {
|
||||||
|
val room = repository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
|
||||||
|
if (member.id!! != room.member!!.id!!) {
|
||||||
|
throw SodaException("잘못된 요청입니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
roomInfo.removeDonationMessage(request.messageUUID)
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserProfile(roomId: Long, userId: Long, member: Member): GetLiveRoomUserProfileResponse {
|
||||||
|
val room = repository.getLiveRoom(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val user = memberRepository.findByIdOrNull(userId)
|
||||||
|
?: throw SodaException("잘못된 요청입니다.")
|
||||||
|
|
||||||
|
val isFollowing = if (user.role == MemberRole.CREATOR) {
|
||||||
|
explorerQueryRepository
|
||||||
|
.getNotificationUserIds(userId)
|
||||||
|
.contains(member.id!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조회 하는 유저
|
||||||
|
val memberResponse = LiveRoomMember(member)
|
||||||
|
// 조회 당하는 유저
|
||||||
|
val userResponse = LiveRoomMember(user)
|
||||||
|
|
||||||
|
val isSpeaker = if (
|
||||||
|
room.member!!.id!! != userId &&
|
||||||
|
(room.member!!.id!! == member.id!! || roomInfo.managerList.contains(memberResponse))
|
||||||
|
) {
|
||||||
|
roomInfo.speakerList.contains(userResponse)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val isManager = if (room.member!!.id!! != userId && room.member!!.id!! == member.id!!) {
|
||||||
|
roomInfo.managerList.contains(userResponse)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetLiveRoomUserProfileResponse(
|
||||||
|
userId = user.id!!,
|
||||||
|
nickname = user.nickname,
|
||||||
|
profileUrl = if (user.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${user.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
},
|
||||||
|
gender = if (user.gender == Gender.FEMALE) "여" else if (user.gender == Gender.MALE) "남" else "미",
|
||||||
|
instagramUrl = user.instagramUrl,
|
||||||
|
youtubeUrl = user.youtubeUrl,
|
||||||
|
websiteUrl = user.websiteUrl,
|
||||||
|
blogUrl = user.blogUrl,
|
||||||
|
introduce = user.introduce,
|
||||||
|
tags = "",
|
||||||
|
isSpeaker = isSpeaker,
|
||||||
|
isManager = isManager,
|
||||||
|
isFollowing = isFollowing,
|
||||||
|
isBlock = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDonationTotal(roomId: Long): GetLiveRoomDonationTotalResponse {
|
||||||
|
return GetLiveRoomDonationTotalResponse(
|
||||||
|
totalDonationCoin = repository.getDonationTotal(roomId = roomId) ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest) {
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val account = memberRepository.findByIdOrNull(request.accountId)
|
||||||
|
?: throw SodaException("로그인 정보를 확인해 주세요.")
|
||||||
|
|
||||||
|
if (roomInfo.speakerCount > 9) {
|
||||||
|
throw SodaException("스피커 정원이 초과하였습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
roomInfo.removeListener(account)
|
||||||
|
roomInfo.removeManager(account)
|
||||||
|
roomInfo.addSpeaker(account)
|
||||||
|
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener(request: SetManagerOrSpeakerOrAudienceRequest) {
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val member = memberRepository.findByIdOrNull(request.accountId)
|
||||||
|
?: throw SodaException("로그인 정보를 확인해 주세요.")
|
||||||
|
|
||||||
|
roomInfo.removeSpeaker(member)
|
||||||
|
roomInfo.removeManager(member)
|
||||||
|
roomInfo.addListener(member)
|
||||||
|
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setManager(request: SetManagerOrSpeakerOrAudienceRequest, member: Member) {
|
||||||
|
val room = repository.getLiveRoom(request.roomId) ?: throw SodaException("잘못된 요청입니다.")
|
||||||
|
if (room.member!!.id!! != member.id!!) {
|
||||||
|
throw SodaException("권한이 없습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = memberRepository.findByIdOrNull(request.accountId) ?: throw SodaException("해당하는 유저가 없습니다.")
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
val roomAccountResponse = LiveRoomMember(member = user)
|
||||||
|
if (roomInfo.managerList.contains(roomAccountResponse)) {
|
||||||
|
throw SodaException("이미 매니저 입니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!roomInfo.speakerList.contains(roomAccountResponse) &&
|
||||||
|
!roomInfo.listenerList.contains(roomAccountResponse)
|
||||||
|
) {
|
||||||
|
throw SodaException("해당하는 유저가 없습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
roomInfo.removeListener(user)
|
||||||
|
roomInfo.removeSpeaker(user)
|
||||||
|
roomInfo.addManager(user)
|
||||||
|
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun donation(request: LiveRoomDonationRequest, member: Member) {
|
||||||
|
val room = repository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
|
||||||
|
val host = room.member ?: throw SodaException("잘못된 요청입니다.")
|
||||||
|
|
||||||
|
if (host.role != MemberRole.CREATOR) {
|
||||||
|
throw SodaException("비비드넥스트와 계약한\n요즘친구에게만 후원을 하실 수 있습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
canPaymentService.spendCan(
|
||||||
|
memberId = member.id!!,
|
||||||
|
needCan = request.can,
|
||||||
|
canUsage = CanUsage.DONATION,
|
||||||
|
liveRoom = room,
|
||||||
|
container = request.container
|
||||||
|
)
|
||||||
|
|
||||||
|
if (request.message.isNotBlank()) {
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(room.id!!)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
roomInfo.addDonationMessage(
|
||||||
|
nickname = member.nickname,
|
||||||
|
can = request.can,
|
||||||
|
donationMessage = request.message
|
||||||
|
)
|
||||||
|
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun refundDonation(roomId: Long, member: Member) {
|
||||||
|
val donator = memberRepository.findByIdOrNull(member.id)
|
||||||
|
?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.")
|
||||||
|
|
||||||
|
val useCan = canRepository.getCanUsedForLiveRoomNotRefund(
|
||||||
|
memberId = member.id!!,
|
||||||
|
roomId = roomId,
|
||||||
|
canUsage = CanUsage.DONATION
|
||||||
|
) ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.")
|
||||||
|
useCan.isRefund = true
|
||||||
|
|
||||||
|
val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!)
|
||||||
|
useCoinCalculates.forEach {
|
||||||
|
it.status = UseCanCalculateStatus.REFUND
|
||||||
|
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
|
||||||
|
charge.title = "${it.can} 코인"
|
||||||
|
charge.useCan = useCan
|
||||||
|
|
||||||
|
when (it.paymentGateway) {
|
||||||
|
PaymentGateway.PG -> donator.pgRewardCan += charge.rewardCan
|
||||||
|
PaymentGateway.GOOGLE_IAP -> donator.googleRewardCan += charge.rewardCan
|
||||||
|
PaymentGateway.APPLE_IAP -> donator.appleRewardCan += charge.rewardCan
|
||||||
|
}
|
||||||
|
charge.member = donator
|
||||||
|
|
||||||
|
val payment = Payment(
|
||||||
|
status = PaymentStatus.COMPLETE,
|
||||||
|
paymentGateway = it.paymentGateway
|
||||||
|
)
|
||||||
|
payment.method = "환불"
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
chargeRepository.save(charge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDonationStatus(roomId: Long, member: Member): GetLiveRoomDonationStatusResponse {
|
||||||
|
val room = repository.getLiveRoom(roomId) ?: throw SodaException("잘못된 요청입니다.")
|
||||||
|
val donationList = repository.getDonationList(roomId = room.id!!, cloudFrontHost = cloudFrontHost)
|
||||||
|
val totalCan = donationList.sumOf { it.can }
|
||||||
|
|
||||||
|
return GetLiveRoomDonationStatusResponse(
|
||||||
|
donationList = donationList,
|
||||||
|
totalCount = donationList.size,
|
||||||
|
totalCan = totalCan
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun quitRoom(roomId: Long, member: Member) {
|
||||||
|
val room = repository.getLiveRoom(roomId)
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(roomId)
|
||||||
|
if (roomInfo != null) {
|
||||||
|
if (room?.member != null && room.member!! == member) {
|
||||||
|
room.isActive = false
|
||||||
|
kickOutService.deleteKickOutData(roomId = room.id!!)
|
||||||
|
roomInfoRepository.deleteById(roomInfo.roomId)
|
||||||
|
} else {
|
||||||
|
roomInfo.removeSpeaker(member)
|
||||||
|
roomInfo.removeListener(member)
|
||||||
|
roomInfo.removeManager(member)
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
|
data class SetManagerOrSpeakerOrAudienceRequest(
|
||||||
|
val roomId: Long,
|
||||||
|
val accountId: Long
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.donation
|
||||||
|
|
||||||
|
data class DeleteLiveRoomDonationMessage(
|
||||||
|
val roomId: Long,
|
||||||
|
val messageUUID: String
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.donation
|
||||||
|
|
||||||
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
|
|
||||||
|
data class GetLiveRoomDonationStatusResponse(
|
||||||
|
val donationList: List<GetLiveRoomDonationItem>,
|
||||||
|
val totalCount: Int,
|
||||||
|
val totalCan: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetLiveRoomDonationItem @QueryProjection constructor(
|
||||||
|
val profileImage: String,
|
||||||
|
val nickname: String,
|
||||||
|
val userId: Long,
|
||||||
|
val can: Int
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.donation
|
||||||
|
|
||||||
|
data class GetLiveRoomDonationTotalResponse(val totalDonationCoin: Int)
|
|
@ -0,0 +1,8 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.donation
|
||||||
|
|
||||||
|
data class LiveRoomDonationRequest(
|
||||||
|
val roomId: Long,
|
||||||
|
val can: Int,
|
||||||
|
val container: String,
|
||||||
|
val message: String = ""
|
||||||
|
)
|
|
@ -0,0 +1,25 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.info
|
||||||
|
|
||||||
|
data class GetRoomInfoResponse(
|
||||||
|
val roomId: Long,
|
||||||
|
val title: String,
|
||||||
|
val notice: String,
|
||||||
|
val coverImageUrl: String,
|
||||||
|
val channelName: String,
|
||||||
|
val rtcToken: String,
|
||||||
|
val rtmToken: String,
|
||||||
|
val managerId: Long,
|
||||||
|
val managerNickname: String,
|
||||||
|
val managerProfileUrl: String,
|
||||||
|
val isFollowingManager: Boolean,
|
||||||
|
val participantsCount: Int,
|
||||||
|
val totalAvailableParticipantsCount: Int,
|
||||||
|
val speakerList: List<LiveRoomMember>,
|
||||||
|
val listenerList: List<LiveRoomMember>,
|
||||||
|
val managerList: List<LiveRoomMember>,
|
||||||
|
val donationRankingTop3UserIds: List<Long>,
|
||||||
|
val isRadioMode: Boolean = false,
|
||||||
|
val isAvailableDonation: Boolean = false,
|
||||||
|
val isPrivateRoom: Boolean = false,
|
||||||
|
val password: String? = null
|
||||||
|
)
|
|
@ -0,0 +1,32 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.kickout
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.redis.core.RedisHash
|
||||||
|
|
||||||
|
@RedisHash("live_room_kick_out")
|
||||||
|
data class LiveRoomKickOut(
|
||||||
|
@Id
|
||||||
|
val roomId: Long,
|
||||||
|
var userList: MutableList<LiveRoomKickOutUser> = mutableListOf()
|
||||||
|
) {
|
||||||
|
fun kickOut(userId: Long) {
|
||||||
|
var liveRoomKickOutUser = userList.find { it.userId == userId }
|
||||||
|
if (liveRoomKickOutUser == null) {
|
||||||
|
liveRoomKickOutUser = LiveRoomKickOutUser(userId)
|
||||||
|
} else {
|
||||||
|
liveRoomKickOutUser.plusCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
userList.removeIf { it.userId == userId }
|
||||||
|
userList.add(liveRoomKickOutUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LiveRoomKickOutUser(
|
||||||
|
val userId: Long,
|
||||||
|
var count: Int = 1
|
||||||
|
) {
|
||||||
|
fun plusCount() {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.kickout
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/live/room/kick-out")
|
||||||
|
class LiveRoomKickOutController(private val service: LiveRoomKickOutService) {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
fun liveRoomKickOut(
|
||||||
|
@RequestBody request: LiveRoomKickOutRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.kickOut(request = request, member = member))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.kickout
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository
|
||||||
|
|
||||||
|
interface LiveRoomKickOutRedisRepository : CrudRepository<LiveRoomKickOut, Long>
|
|
@ -0,0 +1,6 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.kickout
|
||||||
|
|
||||||
|
data class LiveRoomKickOutRequest(
|
||||||
|
val roomId: Long,
|
||||||
|
val userId: Long
|
||||||
|
)
|
|
@ -0,0 +1,61 @@
|
||||||
|
package kr.co.vividnext.sodalive.live.room.kickout
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.live.room.LiveRoomRepository
|
||||||
|
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
|
||||||
|
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class LiveRoomKickOutService(
|
||||||
|
private val roomInfoRepository: LiveRoomInfoRedisRepository,
|
||||||
|
private val repository: LiveRoomKickOutRedisRepository,
|
||||||
|
private val accountRepository: MemberRepository,
|
||||||
|
private val roomRepository: LiveRoomRepository
|
||||||
|
) {
|
||||||
|
fun kickOut(request: LiveRoomKickOutRequest, member: Member) {
|
||||||
|
val room = roomRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
|
||||||
|
val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
|
||||||
|
?: throw SodaException("해당하는 라이브의 정보가 없습니다.")
|
||||||
|
|
||||||
|
if (room.member == null || room.member!!.id == null) {
|
||||||
|
throw SodaException("해당하는 라이브가 없습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomInfo.managerList.contains(LiveRoomMember(member)) && room.member!!.id != member.id) {
|
||||||
|
throw SodaException("권한이 없습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var liveRoomKickOut = repository.findByIdOrNull(request.roomId)
|
||||||
|
if (liveRoomKickOut == null) {
|
||||||
|
liveRoomKickOut = repository.save(LiveRoomKickOut(roomId = request.roomId))
|
||||||
|
}
|
||||||
|
|
||||||
|
liveRoomKickOut.kickOut(request.userId)
|
||||||
|
repository.save(liveRoomKickOut)
|
||||||
|
|
||||||
|
val kickOutUser = accountRepository.findByIdOrNull(request.userId)
|
||||||
|
if (kickOutUser != null) {
|
||||||
|
roomInfo.removeSpeaker(kickOutUser)
|
||||||
|
roomInfo.removeListener(kickOutUser)
|
||||||
|
roomInfo.removeManager(kickOutUser)
|
||||||
|
roomInfoRepository.save(roomInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKickOutCount(roomId: Long, userId: Long): Int {
|
||||||
|
val liveRoomKickOut = repository.findByIdOrNull(roomId) ?: return 0
|
||||||
|
|
||||||
|
val findUser = liveRoomKickOut.userList.find { it.userId == userId } ?: return 0
|
||||||
|
return findUser.count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteKickOutData(roomId: Long) {
|
||||||
|
repository.deleteById(roomId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.member
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
|
||||||
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
||||||
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
|
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
@ -72,4 +74,44 @@ class MemberController(private val service: MemberService) {
|
||||||
|
|
||||||
ApiResponse.ok(service.getMyPage(member, container))
|
ApiResponse.ok(service.getMyPage(member, container))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/creator/follow")
|
||||||
|
fun creatorFollow(
|
||||||
|
@RequestBody request: CreatorFollowRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.creatorFollow(creatorId = request.creatorId, memberId = member.id!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/creator/unfollow")
|
||||||
|
fun creatorUnFollow(
|
||||||
|
@RequestBody request: CreatorFollowRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.creatorUnFollow(creatorId = request.creatorId, memberId = member.id!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/block")
|
||||||
|
fun memberBlock(
|
||||||
|
@RequestBody request: MemberBlockRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.memberBlock(request = request, memberId = member.id!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/unblock")
|
||||||
|
fun memberUnBlock(
|
||||||
|
@RequestBody request: MemberBlockRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,11 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||||
|
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||||
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
|
||||||
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||||
import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse
|
import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
@ -39,6 +44,8 @@ class MemberService(
|
||||||
private val repository: MemberRepository,
|
private val repository: MemberRepository,
|
||||||
private val stipulationRepository: StipulationRepository,
|
private val stipulationRepository: StipulationRepository,
|
||||||
private val stipulationAgreeRepository: StipulationAgreeRepository,
|
private val stipulationAgreeRepository: StipulationAgreeRepository,
|
||||||
|
private val creatorFollowingRepository: CreatorFollowingRepository,
|
||||||
|
private val blockMemberRepository: BlockMemberRepository,
|
||||||
|
|
||||||
private val memberNotificationService: MemberNotificationService,
|
private val memberNotificationService: MemberNotificationService,
|
||||||
|
|
||||||
|
@ -264,4 +271,61 @@ class MemberService(
|
||||||
|
|
||||||
return MemberAdapter(member)
|
return MemberAdapter(member)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun creatorFollow(creatorId: Long, memberId: Long) {
|
||||||
|
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (creatorFollowing == null) {
|
||||||
|
val creator = repository.findByIdOrNull(creatorId) ?: throw SodaException("크리에이터 정보를 확인해주세요.")
|
||||||
|
val member = repository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
creatorFollowingRepository.save(CreatorFollowing(creator = creator, member = member))
|
||||||
|
} else {
|
||||||
|
creatorFollowing.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun creatorUnFollow(creatorId: Long, memberId: Long) {
|
||||||
|
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (creatorFollowing != null) {
|
||||||
|
creatorFollowing.isActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun memberBlock(request: MemberBlockRequest, memberId: Long) {
|
||||||
|
var blockMember = blockMemberRepository.getBlockAccount(
|
||||||
|
blockedMemberId = request.blockMemberId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (blockMember == null) {
|
||||||
|
blockMember = BlockMember(
|
||||||
|
blockedMemberId = request.blockMemberId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
blockMemberRepository.save(blockMember)
|
||||||
|
} else {
|
||||||
|
blockMember.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun memberUnBlock(request: MemberBlockRequest, memberId: Long) {
|
||||||
|
val blockMember = blockMemberRepository.getBlockAccount(
|
||||||
|
blockedMemberId = request.blockMemberId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (blockMember != null) {
|
||||||
|
blockMember.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.block
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Entity
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class BlockMember(
|
||||||
|
val blockedMemberId: Long,
|
||||||
|
val memberId: Long
|
||||||
|
) : BaseEntity() {
|
||||||
|
var isActive: Boolean = true
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.block
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface BlockMemberRepository : JpaRepository<BlockMember, Long>, BlockMemberQueryRepository
|
||||||
|
|
||||||
|
interface BlockMemberQueryRepository {
|
||||||
|
fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : BlockMemberQueryRepository {
|
||||||
|
override fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? {
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.blockedMemberId.eq(blockedMemberId)
|
||||||
|
.and(blockMember.memberId.eq(memberId))
|
||||||
|
)
|
||||||
|
.orderBy(blockMember.id.desc())
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.block
|
||||||
|
|
||||||
|
data class MemberBlockRequest(val blockMemberId: Long)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.following
|
||||||
|
|
||||||
|
data class CreatorFollowRequest(val creatorId: Long)
|
|
@ -0,0 +1,23 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.following
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class CreatorFollowing(
|
||||||
|
// 유저가 알림받기 한 크리에이터
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "creator_id", nullable = false)
|
||||||
|
var creator: Member,
|
||||||
|
|
||||||
|
// 크리에이터를 알림받기 한 유저
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
var member: Member,
|
||||||
|
|
||||||
|
var isActive: Boolean = true
|
||||||
|
) : BaseEntity()
|
|
@ -0,0 +1,28 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.following
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface CreatorFollowingRepository : JpaRepository<CreatorFollowing, Long>, CreatorFollowingQueryRepository
|
||||||
|
|
||||||
|
interface CreatorFollowingQueryRepository {
|
||||||
|
fun findByCreatorIdAndMemberId(creatorId: Long, memberId: Long): CreatorFollowing?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class CreatorFollowingQueryRepositoryImpl(
|
||||||
|
private val queryFactory: JPAQueryFactory
|
||||||
|
) : CreatorFollowingQueryRepository {
|
||||||
|
override fun findByCreatorIdAndMemberId(creatorId: Long, memberId: Long): CreatorFollowing? {
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(creatorFollowing)
|
||||||
|
.where(
|
||||||
|
creatorFollowing.creator.id.eq(creatorId)
|
||||||
|
.and(creatorFollowing.member.id.eq(memberId))
|
||||||
|
)
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,10 @@ apple:
|
||||||
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
||||||
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
||||||
|
|
||||||
|
agora:
|
||||||
|
appId: ${AGORA_APP_ID}
|
||||||
|
appCertificate: ${AGORA_APP_CERTIFICATE}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
|
|
|
@ -9,6 +9,10 @@ apple:
|
||||||
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
||||||
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
||||||
|
|
||||||
|
agora:
|
||||||
|
appId: ${AGORA_APP_ID}
|
||||||
|
appCertificate: ${AGORA_APP_CERTIFICATE}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
|
|
Loading…
Reference in New Issue