mirror of
				https://github.com/GayPizzaSpecifications/foundation.git
				synced 2025-11-04 11:39:39 +00:00 
			
		
		
		
	Initial Rough Cut of Heimdall Tracking System
This commit is contained in:
		@ -13,18 +13,18 @@ import net.dv8tion.jda.api.entities.TextChannel
 | 
				
			|||||||
import net.dv8tion.jda.api.events.GenericEvent
 | 
					import net.dv8tion.jda.api.events.GenericEvent
 | 
				
			||||||
import net.dv8tion.jda.api.events.ReadyEvent
 | 
					import net.dv8tion.jda.api.events.ReadyEvent
 | 
				
			||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
 | 
					import net.dv8tion.jda.api.events.message.MessageReceivedEvent
 | 
				
			||||||
import net.dv8tion.jda.api.hooks.EventListener
 | 
					import net.dv8tion.jda.api.hooks.EventListener as DiscordEventListener
 | 
				
			||||||
import net.kyori.adventure.text.Component
 | 
					import net.kyori.adventure.text.Component
 | 
				
			||||||
import net.kyori.adventure.text.TextComponent
 | 
					import net.kyori.adventure.text.TextComponent
 | 
				
			||||||
import org.bukkit.event.EventHandler
 | 
					import org.bukkit.event.EventHandler
 | 
				
			||||||
import org.bukkit.event.Listener
 | 
					import org.bukkit.event.Listener as BukkitEventListener
 | 
				
			||||||
import org.bukkit.event.player.PlayerJoinEvent
 | 
					import org.bukkit.event.player.PlayerJoinEvent
 | 
				
			||||||
import org.bukkit.event.player.PlayerQuitEvent
 | 
					import org.bukkit.event.player.PlayerQuitEvent
 | 
				
			||||||
import org.bukkit.plugin.java.JavaPlugin
 | 
					import org.bukkit.plugin.java.JavaPlugin
 | 
				
			||||||
import java.awt.Color
 | 
					import java.awt.Color
 | 
				
			||||||
import kotlin.io.path.inputStream
 | 
					import kotlin.io.path.inputStream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FoundationBifrostPlugin : JavaPlugin(), EventListener, Listener {
 | 
					class FoundationBifrostPlugin : JavaPlugin(), DiscordEventListener, BukkitEventListener {
 | 
				
			||||||
  private lateinit var config: BifrostConfig
 | 
					  private lateinit var config: BifrostConfig
 | 
				
			||||||
  private lateinit var jda: JDA
 | 
					  private lateinit var jda: JDA
 | 
				
			||||||
  private var isDev = false
 | 
					  private var isDev = false
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								foundation-heimdall/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								foundation-heimdall/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					dependencies {
 | 
				
			||||||
 | 
					  implementation("org.postgresql:postgresql:42.3.1")
 | 
				
			||||||
 | 
					  implementation("org.jetbrains.exposed:exposed-jdbc:0.36.2")
 | 
				
			||||||
 | 
					  implementation("org.jetbrains.exposed:exposed-java-time:0.36.2")
 | 
				
			||||||
 | 
					  implementation("com.zaxxer:HikariCP:5.0.0")
 | 
				
			||||||
 | 
					  compileOnly(project(":foundation-core"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun String.sqlSplitStatements(): List<String> {
 | 
				
			||||||
 | 
					  val statements = mutableListOf<String>()
 | 
				
			||||||
 | 
					  val buffer = StringBuilder()
 | 
				
			||||||
 | 
					  fun flush() {
 | 
				
			||||||
 | 
					    val trimmed = buffer.toString().trim()
 | 
				
			||||||
 | 
					    if (trimmed.isNotEmpty()) {
 | 
				
			||||||
 | 
					      statements.add(trimmed)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  for (line in lines()) {
 | 
				
			||||||
 | 
					    if (line.trim() == "--") {
 | 
				
			||||||
 | 
					      flush()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      buffer.append(line).append("\n")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  flush()
 | 
				
			||||||
 | 
					  return statements
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.core.FoundationCorePlugin
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.core.Util
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.buffer.BufferFlushThread
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.buffer.EventBuffer
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.event.PlayerPositionEvent
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.model.HeimdallConfig
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable
 | 
				
			||||||
 | 
					import com.charleskorn.kaml.Yaml
 | 
				
			||||||
 | 
					import com.zaxxer.hikari.HikariConfig
 | 
				
			||||||
 | 
					import com.zaxxer.hikari.HikariDataSource
 | 
				
			||||||
 | 
					import org.bukkit.event.EventHandler
 | 
				
			||||||
 | 
					import org.bukkit.event.Listener
 | 
				
			||||||
 | 
					import org.bukkit.event.player.PlayerMoveEvent
 | 
				
			||||||
 | 
					import org.bukkit.plugin.java.JavaPlugin
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.Database
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.insert
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.transactions.transaction
 | 
				
			||||||
 | 
					import org.postgresql.Driver
 | 
				
			||||||
 | 
					import java.lang.Exception
 | 
				
			||||||
 | 
					import java.time.Instant
 | 
				
			||||||
 | 
					import kotlin.io.path.inputStream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FoundationHeimdallPlugin : JavaPlugin(), Listener {
 | 
				
			||||||
 | 
					  private lateinit var config: HeimdallConfig
 | 
				
			||||||
 | 
					  private lateinit var pool: HikariDataSource
 | 
				
			||||||
 | 
					  internal lateinit var db: Database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private val buffer = EventBuffer()
 | 
				
			||||||
 | 
					  private val bufferFlushThread = BufferFlushThread(this, buffer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun onEnable() {
 | 
				
			||||||
 | 
					    val foundation = server.pluginManager.getPlugin("Foundation") as FoundationCorePlugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val configPath = Util.copyDefaultConfig<FoundationHeimdallPlugin>(
 | 
				
			||||||
 | 
					      slF4JLogger,
 | 
				
			||||||
 | 
					      foundation.pluginDataPath,
 | 
				
			||||||
 | 
					      "heimdall.yaml"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    config = Yaml.default.decodeFromStream(HeimdallConfig.serializer(), configPath.inputStream())
 | 
				
			||||||
 | 
					    if (!config.enabled) {
 | 
				
			||||||
 | 
					      slF4JLogger.info("Heimdall is not enabled.")
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    slF4JLogger.info("Heimdall is enabled.")
 | 
				
			||||||
 | 
					    if (!Driver.isRegistered()) {
 | 
				
			||||||
 | 
					      Driver.register()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pool = HikariDataSource(HikariConfig().apply {
 | 
				
			||||||
 | 
					      jdbcUrl = config.db.url
 | 
				
			||||||
 | 
					      username = config.db.username
 | 
				
			||||||
 | 
					      password = config.db.password
 | 
				
			||||||
 | 
					      schema = "heimdall"
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    val initMigrationContent = FoundationHeimdallPlugin::class.java.getResourceAsStream(
 | 
				
			||||||
 | 
					      "/init.sql"
 | 
				
			||||||
 | 
					    )?.readAllBytes()?.decodeToString() ?: throw RuntimeException("Unable to find Heimdall init.sql")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val statements = initMigrationContent.sqlSplitStatements()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pool.connection.use { conn ->
 | 
				
			||||||
 | 
					      conn.autoCommit = false
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        for (statementAsString in statements) {
 | 
				
			||||||
 | 
					          conn.prepareStatement(statementAsString).use {
 | 
				
			||||||
 | 
					            it.execute()
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        conn.commit()
 | 
				
			||||||
 | 
					      } catch (e: Exception) {
 | 
				
			||||||
 | 
					        conn.rollback()
 | 
				
			||||||
 | 
					        throw e
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        conn.autoCommit = true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db = Database.connect(pool)
 | 
				
			||||||
 | 
					    server.pluginManager.registerEvents(this, this)
 | 
				
			||||||
 | 
					    bufferFlushThread.start()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @EventHandler
 | 
				
			||||||
 | 
					  fun onPlayerMove(event: PlayerMoveEvent) = buffer.push(PlayerPositionEvent(event))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun onDisable() {
 | 
				
			||||||
 | 
					    bufferFlushThread.stop()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.FoundationHeimdallPlugin
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.transactions.transaction
 | 
				
			||||||
 | 
					import java.util.concurrent.atomic.AtomicBoolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BufferFlushThread(val plugin: FoundationHeimdallPlugin, val buffer: EventBuffer) {
 | 
				
			||||||
 | 
					  private val running = AtomicBoolean(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun start() {
 | 
				
			||||||
 | 
					    running.set(true)
 | 
				
			||||||
 | 
					    val thread = Thread {
 | 
				
			||||||
 | 
					      plugin.slF4JLogger.info("Buffer Flusher Started")
 | 
				
			||||||
 | 
					      while (running.get()) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          transaction(plugin.db) {
 | 
				
			||||||
 | 
					            val count = buffer.flush(this)
 | 
				
			||||||
 | 
					            if (count > 0) {
 | 
				
			||||||
 | 
					              plugin.slF4JLogger.info("Flushed $count Events")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (e: Exception) {
 | 
				
			||||||
 | 
					          plugin.slF4JLogger.warn("Failed to flush buffer.", e)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Thread.sleep(5000)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      plugin.slF4JLogger.info("Buffer Flusher Stopped")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    thread.name = "Heimdall Buffer Flush"
 | 
				
			||||||
 | 
					    thread.isDaemon = false
 | 
				
			||||||
 | 
					    thread.start()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun stop() {
 | 
				
			||||||
 | 
					    running.set(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.event.HeimdallEvent
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.Transaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EventBuffer {
 | 
				
			||||||
 | 
					  private var events = mutableListOf<HeimdallEvent>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun flush(transaction: Transaction): Long {
 | 
				
			||||||
 | 
					    val referenceOfEvents = events
 | 
				
			||||||
 | 
					    this.events = mutableListOf()
 | 
				
			||||||
 | 
					    var count = 0L
 | 
				
			||||||
 | 
					    while (referenceOfEvents.isNotEmpty()) {
 | 
				
			||||||
 | 
					      val event = referenceOfEvents.removeAt(0)
 | 
				
			||||||
 | 
					      event.store(transaction)
 | 
				
			||||||
 | 
					      count++
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return count
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fun push(event: HeimdallEvent) {
 | 
				
			||||||
 | 
					    events.add(event)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.Transaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class HeimdallEvent {
 | 
				
			||||||
 | 
					  abstract fun store(transaction: Transaction)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cloud.kubelet.foundation.heimdall.table.PlayerPositionTable
 | 
				
			||||||
 | 
					import org.bukkit.Location
 | 
				
			||||||
 | 
					import org.bukkit.event.player.PlayerMoveEvent
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.Transaction
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.insert
 | 
				
			||||||
 | 
					import java.time.Instant
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlayerPositionEvent(
 | 
				
			||||||
 | 
					  val playerUniqueIdentity: UUID,
 | 
				
			||||||
 | 
					  val location: Location
 | 
				
			||||||
 | 
					) : HeimdallEvent() {
 | 
				
			||||||
 | 
					  constructor(event: PlayerMoveEvent) : this(event.player.uniqueId, event.to)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override fun store(transaction: Transaction) {
 | 
				
			||||||
 | 
					    transaction.apply {
 | 
				
			||||||
 | 
					      PlayerPositionTable.insert {
 | 
				
			||||||
 | 
					        it[time] = Instant.now()
 | 
				
			||||||
 | 
					        it[player] = playerUniqueIdentity
 | 
				
			||||||
 | 
					        it[world] = location.world.uid
 | 
				
			||||||
 | 
					        it[x] = location.x
 | 
				
			||||||
 | 
					        it[y] = location.y
 | 
				
			||||||
 | 
					        it[z] = location.z
 | 
				
			||||||
 | 
					        it[pitch] = location.pitch.toDouble()
 | 
				
			||||||
 | 
					        it[yaw] = location.yaw.toDouble()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class HeimdallConfig(
 | 
				
			||||||
 | 
					  val enabled: Boolean = false,
 | 
				
			||||||
 | 
					  val db: DbConfig
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class DbConfig(
 | 
				
			||||||
 | 
					  val url: String,
 | 
				
			||||||
 | 
					  val username: String,
 | 
				
			||||||
 | 
					  val password: String
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package cloud.kubelet.foundation.heimdall.table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.*
 | 
				
			||||||
 | 
					import org.jetbrains.exposed.sql.javatime.timestamp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object PlayerPositionTable : Table("player_positions") {
 | 
				
			||||||
 | 
					  val time = timestamp("time")
 | 
				
			||||||
 | 
					  val world = uuid("world")
 | 
				
			||||||
 | 
					  val player = uuid("player")
 | 
				
			||||||
 | 
					  val x = double("x")
 | 
				
			||||||
 | 
					  val y = double("y")
 | 
				
			||||||
 | 
					  val z = double("z")
 | 
				
			||||||
 | 
					  val pitch = double("pitch")
 | 
				
			||||||
 | 
					  val yaw = double("yaw")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								foundation-heimdall/src/main/resources/heimdall.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								foundation-heimdall/src/main/resources/heimdall.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					# Whether Heimdall should be enabled for tracking events.
 | 
				
			||||||
 | 
					enabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Database connection information.
 | 
				
			||||||
 | 
					db:
 | 
				
			||||||
 | 
					  # JDBC URL
 | 
				
			||||||
 | 
					  url: "jdbc:postgresql://localhost/foundation"
 | 
				
			||||||
 | 
					  # JDBC Username
 | 
				
			||||||
 | 
					  username: "foundation"
 | 
				
			||||||
 | 
					  # JDBC Password
 | 
				
			||||||
 | 
					  password: "foundation"
 | 
				
			||||||
							
								
								
									
										20
									
								
								foundation-heimdall/src/main/resources/init.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								foundation-heimdall/src/main/resources/init.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					--
 | 
				
			||||||
 | 
					create extension if not exists "uuid-ossp";
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					create extension if not exists timescaledb;
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					create schema if not exists heimdall;
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					create table if not exists heimdall.player_positions (
 | 
				
			||||||
 | 
					    time timestamp not null,
 | 
				
			||||||
 | 
					    player uuid not null,
 | 
				
			||||||
 | 
					    world uuid not null,
 | 
				
			||||||
 | 
					    x double precision not null,
 | 
				
			||||||
 | 
					    y double precision not null,
 | 
				
			||||||
 | 
					    z double precision not null,
 | 
				
			||||||
 | 
					    pitch double precision not null,
 | 
				
			||||||
 | 
					    yaw double precision not null,
 | 
				
			||||||
 | 
					    PRIMARY KEY (time, player, world)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					select create_hypertable('heimdall.player_positions', 'time', 'player', 4,  if_not_exists => TRUE);
 | 
				
			||||||
							
								
								
									
										10
									
								
								foundation-heimdall/src/main/resources/plugin.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								foundation-heimdall/src/main/resources/plugin.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					name: Foundation-Heimdall
 | 
				
			||||||
 | 
					version: '${version}'
 | 
				
			||||||
 | 
					main: cloud.kubelet.foundation.heimdall.FoundationHeimdallPlugin
 | 
				
			||||||
 | 
					api-version: 1.18
 | 
				
			||||||
 | 
					prefix: Foundation-Heimdall
 | 
				
			||||||
 | 
					load: STARTUP
 | 
				
			||||||
 | 
					depend:
 | 
				
			||||||
 | 
					  - Foundation
 | 
				
			||||||
 | 
					authors:
 | 
				
			||||||
 | 
					  - kubelet
 | 
				
			||||||
@ -3,4 +3,5 @@ rootProject.name = "foundation"
 | 
				
			|||||||
include(
 | 
					include(
 | 
				
			||||||
  ":foundation-core",
 | 
					  ":foundation-core",
 | 
				
			||||||
  ":foundation-bifrost",
 | 
					  ":foundation-bifrost",
 | 
				
			||||||
 | 
					  ":foundation-heimdall",
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user