Fixed Member Caching, Message#member, Member#roles
This commit is contained in:
parent
71c1499c1d
commit
70824135a7
13 changed files with 103 additions and 85 deletions
|
@ -1,16 +1,15 @@
|
||||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||||
import cache from '../../models/cache.ts'
|
|
||||||
import { Guild } from '../../structures/guild.ts'
|
import { Guild } from '../../structures/guild.ts'
|
||||||
import { User } from '../../structures/user.ts'
|
import { User } from '../../structures/user.ts'
|
||||||
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
import { GuildBanRemovePayload } from '../../types/gateway.ts'
|
||||||
|
|
||||||
export const guildBanRemove: GatewayEventHandler = (
|
export const guildBanRemove: GatewayEventHandler = async (
|
||||||
gateway: Gateway,
|
gateway: Gateway,
|
||||||
d: GuildBanRemovePayload
|
d: GuildBanRemovePayload
|
||||||
) => {
|
) => {
|
||||||
const guild: Guild = cache.get('guild', d.guild_id)
|
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
|
||||||
const user: User =
|
const user: User =
|
||||||
cache.get('user', d.user.id) ?? new User(gateway.client, d.user)
|
await gateway.client.users.get(d.user.id) ?? new User(gateway.client, d.user)
|
||||||
|
|
||||||
if (guild !== undefined) {
|
if (guild !== undefined) {
|
||||||
gateway.client.emit('guildBanRemove', guild, user)
|
gateway.client.emit('guildBanRemove', guild, user)
|
||||||
|
|
|
@ -17,11 +17,26 @@ export const messageCreate: GatewayEventHandler = async (
|
||||||
const user = new User(gateway.client, d.author)
|
const user = new User(gateway.client, d.author)
|
||||||
await gateway.client.users.set(d.author.id, d.author)
|
await gateway.client.users.set(d.author.id, d.author)
|
||||||
let guild
|
let guild
|
||||||
|
let member
|
||||||
if (d.guild_id !== undefined) {
|
if (d.guild_id !== undefined) {
|
||||||
guild = await gateway.client.guilds.get(d.guild_id)
|
guild = await gateway.client.guilds.get(d.guild_id)
|
||||||
}
|
}
|
||||||
|
if (guild !== undefined && d.member !== undefined) {
|
||||||
|
d.member.user = d.author
|
||||||
|
await guild.members.set(d.author.id, d.member)
|
||||||
|
member = await guild.members.get(d.author.id)
|
||||||
|
}
|
||||||
const mentions = new MessageMentions()
|
const mentions = new MessageMentions()
|
||||||
const message = new Message(gateway.client, d, channel as any, user, mentions)
|
const message = new Message(gateway.client, d, channel as any, user, mentions)
|
||||||
|
message.member = member
|
||||||
if (guild !== undefined) message.guild = guild
|
if (guild !== undefined) message.guild = guild
|
||||||
|
if (message.member !== undefined) {
|
||||||
|
if(message.member.user === undefined) {
|
||||||
|
const user = await gateway.client.users.get(message.member.id)
|
||||||
|
if(user !== undefined) {
|
||||||
|
message.member.user = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
gateway.client.emit('messageCreate', message)
|
gateway.client.emit('messageCreate', message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { Collection } from "../utils/collection.ts";
|
||||||
|
|
||||||
export class BaseManager<T, T2> {
|
export class BaseManager<T, T2> {
|
||||||
client: Client
|
client: Client
|
||||||
|
/** Cache Name or Key used to differentiate caches */
|
||||||
cacheName: string
|
cacheName: string
|
||||||
|
/** Which data type does this cache have */
|
||||||
DataType: any
|
DataType: any
|
||||||
|
|
||||||
constructor (client: Client, cacheName: string, DataType: any) {
|
constructor (client: Client, cacheName: string, DataType: any) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { User } from "../structures/user.ts";
|
||||||
import { Client } from "../models/client.ts";
|
import { Client } from "../models/client.ts";
|
||||||
import { Guild } from "../structures/guild.ts";
|
import { Guild } from "../structures/guild.ts";
|
||||||
import { Member } from "../structures/member.ts";
|
import { Member } from "../structures/member.ts";
|
||||||
|
@ -13,11 +14,29 @@ export class MembersManager extends BaseManager<MemberPayload, Member> {
|
||||||
this.guild = guild
|
this.guild = guild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get (key: string): Promise<Member | undefined> {
|
||||||
|
const raw = await this._get(key)
|
||||||
|
if (raw === undefined) return
|
||||||
|
const user = new User(this.client, raw.user)
|
||||||
|
const res = new this.DataType(this.client, raw, user)
|
||||||
|
for (const roleid of res.roleIDs as string[]) {
|
||||||
|
const role = await this.guild.roles.get(roleid)
|
||||||
|
if(role !== undefined) res.roles.push(role)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
async fetch(id: string): Promise<Member> {
|
async fetch(id: string): Promise<Member> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
this.client.rest.get(GUILD_MEMBER(this.guild.id, id)).then(data => {
|
this.client.rest.get(GUILD_MEMBER(this.guild.id, id)).then(async data => {
|
||||||
this.set(id, data as MemberPayload)
|
await this.set(id, data as MemberPayload)
|
||||||
resolve(new Member(this.client, data as MemberPayload))
|
const user: User = new User(this.client, data.user)
|
||||||
|
const res = new Member(this.client, data as MemberPayload, user)
|
||||||
|
for (const roleid of res.roleIDs as string[]) {
|
||||||
|
const role = await this.guild.roles.get(roleid)
|
||||||
|
if(role !== undefined) res.roles.push(role)
|
||||||
|
}
|
||||||
|
resolve(res)
|
||||||
}).catch(e => reject(e))
|
}).catch(e => reject(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
let caches: any = {}
|
|
||||||
|
|
||||||
const get = (cacheName: string, key: string): any => {
|
|
||||||
const gotCache: Map<string, any> = caches[cacheName]
|
|
||||||
if (gotCache === undefined || !(gotCache instanceof Map)) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const gotMap = gotCache.get(key)
|
|
||||||
return gotMap
|
|
||||||
}
|
|
||||||
|
|
||||||
const set = (cacheName: string, key: string, value: any): any => {
|
|
||||||
let gotCache: Map<string, any> = caches[cacheName]
|
|
||||||
if (gotCache === undefined || !(gotCache instanceof Map)) {
|
|
||||||
gotCache = caches[cacheName] = new Map<string, any>()
|
|
||||||
}
|
|
||||||
|
|
||||||
gotCache.set(key, value)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
const del = (cacheName: string, key: string): boolean | undefined => {
|
|
||||||
const gotCache: Map<string, any> = caches[cacheName]
|
|
||||||
if (gotCache === undefined || !(gotCache instanceof Map)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotCache.delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteCache = (cacheName: string): void => {
|
|
||||||
const gotCache = caches[cacheName]
|
|
||||||
if (gotCache === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete caches[cacheName]
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetCaches = (): void => {
|
|
||||||
caches = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default { get, set, del, deleteCache, resetCaches }
|
|
||||||
export { get, set, del, deleteCache, resetCaches }
|
|
|
@ -13,12 +13,19 @@ import { ActivityGame, ClientActivity, ClientPresence } from "../structures/pres
|
||||||
|
|
||||||
/** Some Client Options to modify behaviour */
|
/** Some Client Options to modify behaviour */
|
||||||
export interface ClientOptions {
|
export interface ClientOptions {
|
||||||
|
/** Token of the Bot/User */
|
||||||
token?: string
|
token?: string
|
||||||
|
/** Gateway Intents */
|
||||||
intents?: GatewayIntents[]
|
intents?: GatewayIntents[]
|
||||||
|
/** Cache Adapter to use, defaults to Collections one */
|
||||||
cache?: ICacheAdapter,
|
cache?: ICacheAdapter,
|
||||||
|
/** Force New Session and don't use cached Session (by persistent caching) */
|
||||||
forceNewSession?: boolean,
|
forceNewSession?: boolean,
|
||||||
|
/** Startup presence of client */
|
||||||
presence?: ClientPresence | ClientActivity | ActivityGame
|
presence?: ClientPresence | ClientActivity | ActivityGame
|
||||||
|
/** Whether it's a bot user or not? Use this if selfbot! */
|
||||||
bot?: boolean
|
bot?: boolean
|
||||||
|
/** Force all requests to Canary API */
|
||||||
canary?: boolean
|
canary?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,22 +33,34 @@ export interface ClientOptions {
|
||||||
* Discord Client.
|
* Discord Client.
|
||||||
*/
|
*/
|
||||||
export class Client extends EventEmitter {
|
export class Client extends EventEmitter {
|
||||||
|
/** Gateway object */
|
||||||
gateway?: Gateway
|
gateway?: Gateway
|
||||||
|
/** REST Manager - used to make all requests */
|
||||||
rest: RESTManager = new RESTManager(this)
|
rest: RESTManager = new RESTManager(this)
|
||||||
|
/** User which Client logs in to, undefined until logs in */
|
||||||
user?: User
|
user?: User
|
||||||
|
/** WebSocket ping of Client */
|
||||||
ping = 0
|
ping = 0
|
||||||
|
/** Token of the Bot/User */
|
||||||
token?: string
|
token?: string
|
||||||
|
/** Cache Adapter */
|
||||||
cache: ICacheAdapter = new DefaultCacheAdapter()
|
cache: ICacheAdapter = new DefaultCacheAdapter()
|
||||||
|
/** Gateway Intents */
|
||||||
intents?: GatewayIntents[]
|
intents?: GatewayIntents[]
|
||||||
|
/** Whether to force new session or not */
|
||||||
forceNewSession?: boolean
|
forceNewSession?: boolean
|
||||||
|
|
||||||
users: UserManager = new UserManager(this)
|
users: UserManager = new UserManager(this)
|
||||||
guilds: GuildManager = new GuildManager(this)
|
guilds: GuildManager = new GuildManager(this)
|
||||||
channels: ChannelsManager = new ChannelsManager(this)
|
channels: ChannelsManager = new ChannelsManager(this)
|
||||||
messages: MessagesManager = new MessagesManager(this)
|
messages: MessagesManager = new MessagesManager(this)
|
||||||
emojis: EmojisManager = new EmojisManager(this)
|
emojis: EmojisManager = new EmojisManager(this)
|
||||||
|
|
||||||
|
/** Whether this client will login as bot user or not */
|
||||||
bot: boolean = true
|
bot: boolean = true
|
||||||
|
/** Whether the REST Manager will use Canary API or not */
|
||||||
canary: boolean = false
|
canary: boolean = false
|
||||||
|
/** Client's presence. Startup one if set before connecting */
|
||||||
presence: ClientPresence = new ClientPresence()
|
presence: ClientPresence = new ClientPresence()
|
||||||
|
|
||||||
constructor (options: ClientOptions = {}) {
|
constructor (options: ClientOptions = {}) {
|
||||||
|
@ -55,11 +74,13 @@ export class Client extends EventEmitter {
|
||||||
if (options.canary === true) this.canary = true
|
if (options.canary === true) this.canary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set Cache Adapter */
|
||||||
setAdapter (adapter: ICacheAdapter): Client {
|
setAdapter (adapter: ICacheAdapter): Client {
|
||||||
this.cache = adapter
|
this.cache = adapter
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Change Presence of Client */
|
||||||
setPresence (presence: ClientPresence | ClientActivity | ActivityGame): void {
|
setPresence (presence: ClientPresence | ClientActivity | ActivityGame): void {
|
||||||
if (presence instanceof ClientPresence) {
|
if (presence instanceof ClientPresence) {
|
||||||
this.presence = presence
|
this.presence = presence
|
||||||
|
@ -67,6 +88,7 @@ export class Client extends EventEmitter {
|
||||||
this.gateway?.sendPresence(this.presence.create())
|
this.gateway?.sendPresence(this.presence.create())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Emit debug event */
|
||||||
debug (tag: string, msg: string): void {
|
debug (tag: string, msg: string): void {
|
||||||
this.emit("debug", `[${tag}] ${msg}`)
|
this.emit("debug", `[${tag}] ${msg}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ export class CommandClient extends Client {
|
||||||
this.emit('commandUsed', { context: ctx })
|
this.emit('commandUsed', { context: ctx })
|
||||||
command.execute(ctx)
|
command.execute(ctx)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (this.texts.ERROR !== undefined) return this.sendProcessedText(msg, this.texts.ERROR, Object.assign(baseReplaces, { error: e.message }))
|
if (this.texts.ERROR !== undefined) this.sendProcessedText(msg, this.texts.ERROR, Object.assign(baseReplaces, { error: e.message }))
|
||||||
this.emit('commandError', { command, parsed, error: e })
|
this.emit('commandError', { command, parsed, error: e })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../models/client.ts'
|
||||||
import * as cache from '../models/cache.ts'
|
|
||||||
|
|
||||||
interface IInit {
|
interface IInit {
|
||||||
useCache?: boolean
|
useCache?: boolean
|
||||||
|
@ -25,18 +24,13 @@ export class Base {
|
||||||
this.useCache = useCache
|
this.useCache = useCache
|
||||||
const cacheID = restURLfuncArgs.join(':')
|
const cacheID = restURLfuncArgs.join(':')
|
||||||
if (this.useCache !== undefined) {
|
if (this.useCache !== undefined) {
|
||||||
const cached = cache.get(this.cacheName ?? this.name, cacheID)
|
const cached = await client.cache.get(this.cacheName ?? this.name, cacheID)
|
||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(endpoint(...restURLfuncArgs), {
|
const jsonParsed = await client.rest.get(endpoint(...restURLfuncArgs))
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${client.token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const jsonParsed = await resp.json()
|
|
||||||
|
|
||||||
return new this(client, jsonParsed)
|
return new this(client, jsonParsed)
|
||||||
}
|
}
|
||||||
|
@ -47,12 +41,7 @@ export class Base {
|
||||||
): Promise<this> {
|
): Promise<this> {
|
||||||
const oldOne = Object.assign(Object.create(this), this)
|
const oldOne = Object.assign(Object.create(this), this)
|
||||||
|
|
||||||
const resp = await fetch(endpoint(...restURLfuncArgs), {
|
const jsonParsed = await client.rest.get(endpoint(...restURLfuncArgs))
|
||||||
headers: {
|
|
||||||
Authorization: `Bot ${client.token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const jsonParsed = await resp.json()
|
|
||||||
|
|
||||||
this.readFromData(jsonParsed)
|
this.readFromData(jsonParsed)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { PresenceUpdatePayload } from '../types/gateway.ts'
|
||||||
import { Base } from './base.ts'
|
import { Base } from './base.ts'
|
||||||
import { Emoji } from './emoji.ts'
|
import { Emoji } from './emoji.ts'
|
||||||
import { VoiceState } from './voiceState.ts'
|
import { VoiceState } from './voiceState.ts'
|
||||||
import cache from '../models/cache.ts'
|
|
||||||
import { RolesManager } from "../managers/roles.ts"
|
import { RolesManager } from "../managers/roles.ts"
|
||||||
import { GuildChannelsManager } from "../managers/guildChannels.ts"
|
import { GuildChannelsManager } from "../managers/guildChannels.ts"
|
||||||
import { MembersManager } from "../managers/members.ts"
|
import { MembersManager } from "../managers/members.ts"
|
||||||
|
@ -156,10 +155,10 @@ export class Guild extends Base {
|
||||||
// data.roles.map(
|
// data.roles.map(
|
||||||
// v => cache.get('role', v.id) ?? new Role(this.client, v)
|
// v => cache.get('role', v.id) ?? new Role(this.client, v)
|
||||||
// ) ?? this.roles
|
// ) ?? this.roles
|
||||||
this.emojis =
|
// this.emojis =
|
||||||
data.emojis.map(
|
// data.emojis.map(
|
||||||
v => cache.get('emoji', v.id) ?? new Emoji(this.client, v)
|
// v => cache.get('emoji', v.id) ?? new Emoji(this.client, v)
|
||||||
) ?? this.emojis
|
// ) ?? this.emojis
|
||||||
this.features = data.features ?? this.features
|
this.features = data.features ?? this.features
|
||||||
this.mfaLevel = data.mfa_level ?? this.mfaLevel
|
this.mfaLevel = data.mfa_level ?? this.mfaLevel
|
||||||
this.systemChannelID = data.system_channel_id ?? this.systemChannelID
|
this.systemChannelID = data.system_channel_id ?? this.systemChannelID
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
import cache from '../models/cache.ts'
|
|
||||||
import { Client } from '../models/client.ts'
|
import { Client } from '../models/client.ts'
|
||||||
import { MemberPayload } from '../types/guild.ts'
|
import { MemberPayload } from '../types/guild.ts'
|
||||||
import { Base } from './base.ts'
|
import { Base } from './base.ts'
|
||||||
|
import { Role } from "./role.ts"
|
||||||
import { User } from './user.ts'
|
import { User } from './user.ts'
|
||||||
|
|
||||||
export class Member extends Base {
|
export class Member extends Base {
|
||||||
id: string
|
id: string
|
||||||
user: User
|
user: User
|
||||||
nick?: string
|
nick?: string
|
||||||
roles: string[]
|
roleIDs: string[]
|
||||||
|
roles: Role[] = []
|
||||||
joinedAt: string
|
joinedAt: string
|
||||||
premiumSince?: string
|
premiumSince?: string
|
||||||
deaf: boolean
|
deaf: boolean
|
||||||
mute: boolean
|
mute: boolean
|
||||||
|
|
||||||
constructor (client: Client, data: MemberPayload) {
|
constructor (client: Client, data: MemberPayload, user: User) {
|
||||||
super(client)
|
super(client)
|
||||||
this.id = data.user.id
|
this.id = data.user.id
|
||||||
this.user =
|
this.user = user
|
||||||
cache.get('user', data.user.id) ?? new User(this.client, data.user)
|
// this.user =
|
||||||
|
// cache.get('user', data.user.id) ?? new User(this.client, data.user)
|
||||||
this.nick = data.nick
|
this.nick = data.nick
|
||||||
this.roles = data.roles
|
this.roleIDs = data.roles
|
||||||
this.joinedAt = data.joined_at
|
this.joinedAt = data.joined_at
|
||||||
this.premiumSince = data.premium_since
|
this.premiumSince = data.premium_since
|
||||||
this.deaf = data.deaf
|
this.deaf = data.deaf
|
||||||
|
@ -32,7 +34,7 @@ export class Member extends Base {
|
||||||
protected readFromData (data: MemberPayload): void {
|
protected readFromData (data: MemberPayload): void {
|
||||||
super.readFromData(data.user)
|
super.readFromData(data.user)
|
||||||
this.nick = data.nick ?? this.nick
|
this.nick = data.nick ?? this.nick
|
||||||
this.roles = data.roles ?? this.roles
|
this.roleIDs = data.roles ?? this.roles
|
||||||
this.joinedAt = data.joined_at ?? this.joinedAt
|
this.joinedAt = data.joined_at ?? this.joinedAt
|
||||||
this.premiumSince = data.premium_since ?? this.premiumSince
|
this.premiumSince = data.premium_since ?? this.premiumSince
|
||||||
this.deaf = data.deaf ?? this.deaf
|
this.deaf = data.deaf ?? this.deaf
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { CommandClient, Intents } from '../../mod.ts';
|
import { CommandClient, Intents } from '../../mod.ts';
|
||||||
import PingCommand from "./cmds/ping.ts";
|
import PingCommand from "./cmds/ping.ts";
|
||||||
|
import UserinfoCommand from "./cmds/userinfo.ts";
|
||||||
import { TOKEN } from './config.ts'
|
import { TOKEN } from './config.ts'
|
||||||
|
|
||||||
const client = new CommandClient({
|
const client = new CommandClient({
|
||||||
|
@ -13,6 +14,9 @@ client.on('ready', () => {
|
||||||
console.log(`[Login] Logged in as ${client.user?.tag}!`)
|
console.log(`[Login] Logged in as ${client.user?.tag}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
client.on("commandError", console.log)
|
||||||
|
|
||||||
client.commands.add(PingCommand)
|
client.commands.add(PingCommand)
|
||||||
|
client.commands.add(UserinfoCommand)
|
||||||
|
|
||||||
client.connect(TOKEN, Intents.All)
|
client.connect(TOKEN, Intents.All)
|
|
@ -3,9 +3,8 @@ import { CommandContext } from "../../models/command.ts";
|
||||||
|
|
||||||
export default class PingCommand extends Command {
|
export default class PingCommand extends Command {
|
||||||
name = "ping"
|
name = "ping"
|
||||||
dmOnly = true
|
|
||||||
|
|
||||||
execute(ctx: CommandContext): void {
|
execute(ctx: CommandContext): void {
|
||||||
ctx.message.reply(`pong! Latency: ${ctx.client.ping}ms`)
|
ctx.message.reply(`Pong! Latency: ${ctx.client.ping}ms`)
|
||||||
}
|
}
|
||||||
}
|
}
|
16
src/test/cmds/userinfo.ts
Normal file
16
src/test/cmds/userinfo.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Command, Member, CommandContext, Embed } from "../../../mod.ts";
|
||||||
|
|
||||||
|
export default class UserinfoCommand extends Command {
|
||||||
|
name = "userinfo"
|
||||||
|
guildOnly = true
|
||||||
|
|
||||||
|
execute(ctx: CommandContext): void {
|
||||||
|
const member: Member = ctx.message.member as any
|
||||||
|
const embed = new Embed()
|
||||||
|
.setTitle(`User Info`)
|
||||||
|
.setAuthor({ name: member.user.tag })
|
||||||
|
.addField("ID", member.id)
|
||||||
|
.addField("Roles", member.roles.map(r => r.name).join(", "))
|
||||||
|
ctx.channel.send(embed)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue