mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
Discord: Start bot, add !start and pin validity check
The bot should be created by the admin and added to a discord server mutual to the intended new user(s). On !start in the server, communication is moved to DMs. Currently !start works, and validity of a given PIN is checked although nothing it done with this yet.
This commit is contained in:
parent
4b11bbe21f
commit
d928df7ab2
@ -14,6 +14,7 @@ import (
|
||||
var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
var discordEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
@ -42,7 +43,7 @@ func (app *appContext) loadConfig() error {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
@ -87,15 +88,17 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
discordEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled {
|
||||
if !emailEnabled && !telegramEnabled && !discordEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
|
@ -546,6 +546,62 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Discord",
|
||||
"description": "Settings for Discord invites/signup/notifications"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||
},
|
||||
"required": {
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require Discord connection on sign-up."
|
||||
},
|
||||
"token": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Discord Bot API Token."
|
||||
},
|
||||
"start_command": {
|
||||
"name": "Start command",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "!start",
|
||||
"description": "Command to start the user verification process."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Discord message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@ -565,6 +621,7 @@
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require telegram connection on sign-up."
|
||||
@ -1140,6 +1197,14 @@
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores telegram user IDs and language preferences."
|
||||
},
|
||||
"discord_users": {
|
||||
"name": "Discord users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores discord user IDs and language preferences."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
146
discord.go
Normal file
146
discord.go
Normal file
@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type DiscordToken struct {
|
||||
Token string
|
||||
ChannelID string
|
||||
UserID string
|
||||
Username string
|
||||
}
|
||||
|
||||
type DiscordDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens map[string]DiscordToken // map of user IDs to tokens.
|
||||
verifiedTokens []DiscordToken
|
||||
languages map[string]string // Store of languages for user channelIDs. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
token := app.config.Section("discord").Key("token").String()
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("token was blank")
|
||||
}
|
||||
bot, err := dg.New("Bot " + token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dd := &DiscordDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
tokens: map[string]DiscordToken{},
|
||||
verifiedTokens: []DiscordToken{},
|
||||
languages: map[string]string{},
|
||||
app: app,
|
||||
}
|
||||
for _, user := range app.storage.discord {
|
||||
if user.Lang != "" {
|
||||
dd.languages[user.ChannelID] = user.Lang
|
||||
}
|
||||
}
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) NewAuthToken(channelID, userID, username string) DiscordToken {
|
||||
pin := genAuthToken()
|
||||
token := DiscordToken{
|
||||
Token: pin,
|
||||
ChannelID: channelID,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) run() {
|
||||
d.bot.AddHandler(d.messageHandler)
|
||||
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages
|
||||
if err := d.bot.Open(); err != nil {
|
||||
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||
return
|
||||
}
|
||||
d.username = d.bot.State.User.Username
|
||||
defer d.bot.Close()
|
||||
<-d.ShutdownChannel
|
||||
d.ShutdownChannel <- "Down"
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) Shutdown() {
|
||||
d.Stopped = true
|
||||
d.ShutdownChannel <- "Down"
|
||||
<-d.ShutdownChannel
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||
if m.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
sects := strings.Split(m.Content, " ")
|
||||
if len(sects) == 0 {
|
||||
return
|
||||
}
|
||||
lang := d.app.storage.lang.chosenTelegramLang
|
||||
if storedLang, ok := d.languages[m.Author.ID]; ok {
|
||||
lang = storedLang
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case d.app.config.Section("telegram").Key("start_command").MustString("!start"):
|
||||
d.commandStart(s, m, lang)
|
||||
default:
|
||||
d.commandPIN(s, m, lang)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
|
||||
d.tokens[m.Author.ID] = token
|
||||
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += d.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
|
||||
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||
token, ok := d.tokens[m.Author.ID]
|
||||
if !ok || token.Token != m.Content {
|
||||
_, err := s.ChannelMessageSendReply(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||
m.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := s.ChannelMessageSendReply(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||
m.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
d.verifiedTokens = append(d.verifiedTokens, token)
|
||||
delete(d.tokens, m.Author.ID)
|
||||
}
|
1
go.mod
1
go.mod
@ -11,6 +11,7 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.23.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect
|
||||
|
5
go.sum
5
go.sum
@ -11,6 +11,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
|
||||
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
@ -148,6 +150,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
||||
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
|
||||
@ -265,6 +269,7 @@ github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
14
main.go
14
main.go
@ -96,6 +96,7 @@ type appContext struct {
|
||||
validator Validator
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
@ -341,6 +342,10 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.storage.loadTelegramUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Telegram users: %v", err)
|
||||
}
|
||||
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
|
||||
if err := app.storage.loadDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Discord users: %v", err)
|
||||
}
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
@ -567,6 +572,15 @@ func start(asDaemon, firstCall bool) {
|
||||
defer app.telegram.Shutdown()
|
||||
}
|
||||
}
|
||||
if discordEnabled {
|
||||
app.discord, err = newDiscordDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||
} else {
|
||||
go app.discord.run()
|
||||
defer app.discord.Shutdown()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
address = "0.0.0.0:8056"
|
||||
|
18
storage.go
18
storage.go
@ -16,13 +16,14 @@ import (
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
@ -37,6 +38,13 @@ type TelegramUser struct {
|
||||
Contact bool // Whether to contact through telegram or not
|
||||
}
|
||||
|
||||
type DiscordUser struct {
|
||||
ChannelID string
|
||||
Username string
|
||||
Lang string
|
||||
Contact bool
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
UserCreated customEmail `json:"userCreated"`
|
||||
InviteExpiry customEmail `json:"inviteExpiry"`
|
||||
@ -765,6 +773,14 @@ func (st *Storage) storeTelegramUsers() error {
|
||||
return storeJSON(st.telegram_path, st.telegram)
|
||||
}
|
||||
|
||||
func (st *Storage) loadDiscordUsers() error {
|
||||
return loadJSON(st.discord_path, &st.discord)
|
||||
}
|
||||
|
||||
func (st *Storage) storeDiscordUsers() error {
|
||||
return storeJSON(st.discord_path, st.discord)
|
||||
}
|
||||
|
||||
func (st *Storage) loadCustomEmails() error {
|
||||
return loadJSON(st.customEmails_path, &st.customEmails)
|
||||
}
|
||||
|
24
telegram.go
24
telegram.go
@ -9,7 +9,7 @@ import (
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
type VerifiedToken struct {
|
||||
type TelegramVerifiedToken struct {
|
||||
Token string
|
||||
ChatID int64
|
||||
Username string
|
||||
@ -21,7 +21,7 @@ type TelegramDaemon struct {
|
||||
bot *tg.BotAPI
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens []VerifiedToken
|
||||
verifiedTokens []TelegramVerifiedToken
|
||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||
link string
|
||||
app *appContext
|
||||
@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
return nil, err
|
||||
}
|
||||
td := &TelegramDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
username: bot.Self.UserName,
|
||||
tokens: []string{},
|
||||
verifiedTokens: []VerifiedToken{},
|
||||
verifiedTokens: []TelegramVerifiedToken{},
|
||||
languages: map[int64]string{},
|
||||
link: "https://t.me/" + bot.Self.UserName,
|
||||
app: app,
|
||||
@ -55,10 +54,7 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
return td, nil
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
func genAuthToken() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pin := make([]rune, 8)
|
||||
for i := range pin {
|
||||
@ -68,10 +64,18 @@ func (t *TelegramDaemon) NewAuthToken() string {
|
||||
pin[i] = runes[rand.Intn(len(runes))]
|
||||
}
|
||||
}
|
||||
t.tokens = append(t.tokens, string(pin))
|
||||
return string(pin)
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
t.tokens = append(t.tokens, pin)
|
||||
return pin
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) run() {
|
||||
t.app.info.Println("Starting Telegram bot daemon")
|
||||
u := tg.NewUpdate(0)
|
||||
@ -222,7 +226,7 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
|
||||
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
|
Loading…
Reference in New Issue
Block a user