more work on message parsing
This commit is contained in:
		
							parent
							
								
									6b5857d382
								
							
						
					
					
						commit
						2ba9b1405f
					
				
					 8 changed files with 226 additions and 41 deletions
				
			
		
							
								
								
									
										11
									
								
								commands/clear.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								commands/clear.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
package commands
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ClearCommand(session *discordgo.Session) {
 | 
			
		||||
  fmt.Print("\n\r\033[H\033[2J")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +21,11 @@ func Setup() {
 | 
			
		|||
    Run: HelpCommand,
 | 
			
		||||
    Description: "command help",
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  commandMap["c"] = Command{
 | 
			
		||||
    Run: ClearCommand,
 | 
			
		||||
    Description: "clear",
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCommand(key string) (Command, bool) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ package commands
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"github.com/Cynosphere/comcord/lib"
 | 
			
		||||
	"github.com/Cynosphere/comcord/state"
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,7 @@ func SendMode(session *discordgo.Session) {
 | 
			
		|||
 | 
			
		||||
  state.SetInPrompt(true)
 | 
			
		||||
 | 
			
		||||
  length := len(session.State.User.Username) + 2
 | 
			
		||||
  length := utf8.RuneCountInString(session.State.User.Username) + 2
 | 
			
		||||
  curLength := state.GetNameLength()
 | 
			
		||||
 | 
			
		||||
  prompt := fmt.Sprintf("[%s]%s", session.State.User.Username, strings.Repeat(" ", (curLength - length) + 1))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								events/main.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								events/main.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
package events
 | 
			
		||||
 | 
			
		||||
import "github.com/bwmarrin/discordgo"
 | 
			
		||||
 | 
			
		||||
func Setup(session *discordgo.Session) {
 | 
			
		||||
  session.AddHandlerOnce(Ready)
 | 
			
		||||
  session.AddHandler(MessageCreate)
 | 
			
		||||
  session.AddHandler(MessageUpdate)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -30,5 +30,24 @@ func MessageCreate(session *discordgo.Session, msg *discordgo.MessageCreate) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func MessageUpdate(session *discordgo.Session, msg *discordgo.MessageUpdate) {
 | 
			
		||||
  if msg.Author.ID == session.State.User.ID {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  channel, err := session.State.Channel(msg.ChannelID)
 | 
			
		||||
  if err != nil {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isDM := channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM
 | 
			
		||||
 | 
			
		||||
  if state.IsInPrompt() {
 | 
			
		||||
    state.AddMessageToQueue(msg.Message)
 | 
			
		||||
  } else {
 | 
			
		||||
    lib.ProcessMessage(session, msg.Message, lib.MessageOptions{NoColor: state.HasNoColor()})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if isDM {
 | 
			
		||||
    state.SetLastDM(msg.ChannelID)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package events
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"github.com/Cynosphere/comcord/state"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +12,7 @@ import (
 | 
			
		|||
func Ready(session *discordgo.Session, event *discordgo.Ready) {
 | 
			
		||||
  fmt.Printf("\rLogged in as: %s\n\r", ansi.Color(fmt.Sprintf("%s (%s)", session.State.User.Username, session.State.User.ID), "yellow"))
 | 
			
		||||
 | 
			
		||||
  state.SetNameLength(len(session.State.User.Username) + 2)
 | 
			
		||||
  state.SetNameLength(utf8.RuneCountInString(session.State.User.Username) + 2)
 | 
			
		||||
 | 
			
		||||
  defaultGuild := state.GetConfigValue("defaultGuild")
 | 
			
		||||
  defaultChannel := state.GetConfigValue("defaultChannel")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										172
									
								
								lib/messages.go
									
										
									
									
									
								
							
							
						
						
									
										172
									
								
								lib/messages.go
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -6,13 +6,15 @@ import (
 | 
			
		|||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"github.com/Cynosphere/comcord/state"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	"github.com/mgutz/ansi"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var /*const*/ REGEX_CODEBLOCK = regexp.MustCompile(`(?i)\x60\x60\x60(?:([a-z0-9_+\-\.]+?)\n)?\n*([^\n].*?)\n*\x60\x60\x60`)
 | 
			
		||||
var REGEX_CODEBLOCK = regexp.MustCompile(`(?i)\x60\x60\x60(?:([a-z0-9_+\-\.]+?)\n)?\n*([^\n].*?)\n*\x60\x60\x60`)
 | 
			
		||||
var REGEX_EMOTE = regexp.MustCompile(`<(?:\x{200b}|&)?a?:(\w+):(\d+)>`)
 | 
			
		||||
 | 
			
		||||
type MessageOptions struct {
 | 
			
		||||
  Content string
 | 
			
		||||
| 
						 | 
				
			
			@ -33,18 +35,74 @@ type MessageOptions struct {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func FormatMessage(session *discordgo.Session, options MessageOptions) {
 | 
			
		||||
 | 
			
		||||
  // TODO: timestamps for pin and join
 | 
			
		||||
  timestamp := options.Timestamp.Format("[15:04:05]")
 | 
			
		||||
 | 
			
		||||
  // TODO: history lines
 | 
			
		||||
 | 
			
		||||
  nameLength := len(options.Name) + 2
 | 
			
		||||
  nameLength := utf8.RuneCountInString(options.Name) + 2
 | 
			
		||||
  stateNameLength := state.GetNameLength()
 | 
			
		||||
  if nameLength > stateNameLength {
 | 
			
		||||
    state.SetNameLength(nameLength)
 | 
			
		||||
    stateNameLength = nameLength
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: replies
 | 
			
		||||
  if options.Reply != nil {
 | 
			
		||||
    nameColor := "cyan+b"
 | 
			
		||||
    if options.Bot {
 | 
			
		||||
      nameColor = "yellow+b"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    headerLength := 6 + utf8.RuneCountInString(options.Reply.Author.Username)
 | 
			
		||||
 | 
			
		||||
    content, _ := options.Reply.ContentWithMoreMentionsReplaced(session)
 | 
			
		||||
    replyContent := strings.ReplaceAll(content, "\n", " ")
 | 
			
		||||
 | 
			
		||||
    // TODO: markdown
 | 
			
		||||
    replyContent = REGEX_EMOTE.ReplaceAllString(replyContent, ":$1:")
 | 
			
		||||
 | 
			
		||||
    attachmentCount := len(options.Reply.Attachments)
 | 
			
		||||
    if attachmentCount > 0 {
 | 
			
		||||
      attachmentPlural := ""
 | 
			
		||||
      if attachmentCount > 1 {
 | 
			
		||||
        attachmentPlural = "s"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      replyContent = strings.TrimSpace(replyContent + fmt.Sprintf(" <%d attachment%s>", attachmentCount, attachmentPlural))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stickerCount := len(options.Reply.StickerItems)
 | 
			
		||||
    if stickerCount > 0 {
 | 
			
		||||
      stickerPlural := ""
 | 
			
		||||
      if stickerCount > 0 {
 | 
			
		||||
        stickerPlural = "s"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      replyContent = strings.TrimSpace(replyContent + fmt.Sprintf(" <%d sticker%s>", stickerCount, stickerPlural))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    length := headerLength + utf8.RuneCountInString(replyContent)
 | 
			
		||||
 | 
			
		||||
    replySymbol := " \u00bb "
 | 
			
		||||
    if !options.NoColor {
 | 
			
		||||
      replySymbol = ansi.Color(replySymbol, "white+b")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    name := fmt.Sprintf("[%s] ", options.Reply.Author.Username)
 | 
			
		||||
    if !options.NoColor {
 | 
			
		||||
      name = ansi.Color(name, nameColor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    moreContent := "\u2026"
 | 
			
		||||
    if !options.NoColor {
 | 
			
		||||
      moreContent = ansi.Color(moreContent, "reset")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if length > 79 {
 | 
			
		||||
      replyContent = replyContent[:79 - headerLength] + moreContent
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fmt.Print(replySymbol, name, replyContent, "\n\r")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if options.IsDump {
 | 
			
		||||
    if options.InHistory {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,12 +122,16 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) {
 | 
			
		|||
 | 
			
		||||
      str := fmt.Sprintf("<%s DUMPs in %d characters of %d word%s in %d line%s>", options.Name, len(options.Content), wordCount, wordsPlural, lineCount, linesPlural)
 | 
			
		||||
 | 
			
		||||
      if options.NoColor {
 | 
			
		||||
        fmt.Print(str)
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "yellow+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // TODO: markdown
 | 
			
		||||
    content := options.Content
 | 
			
		||||
    content = REGEX_EMOTE.ReplaceAllString(content, ":$1:")
 | 
			
		||||
 | 
			
		||||
    if options.IsDM {
 | 
			
		||||
      name := fmt.Sprintf("*%s*", options.Name)
 | 
			
		||||
| 
						 | 
				
			
			@ -77,21 +139,40 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) {
 | 
			
		|||
        name = ansi.Color(name, "red+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Printf("%s %s\x07\n\r", name, options.Content)
 | 
			
		||||
    } else if len(options.Content) > 1 &&
 | 
			
		||||
    (strings.HasPrefix(options.Content, "*") && strings.HasSuffix(options.Content, "*") && !strings.HasPrefix(options.Content, "**") && !strings.HasSuffix(options.Content, "**")) ||
 | 
			
		||||
    (strings.HasPrefix(options.Content, "_") && strings.HasSuffix(options.Content, "_") && !strings.HasPrefix(options.Content, "__") && !strings.HasSuffix(options.Content, "__")) {
 | 
			
		||||
      str := fmt.Sprintf("<%s %s>", options.Name, options.Content[1:len(options.Content)-1])
 | 
			
		||||
      fmt.Printf("%s %s\x07\n\r", name, content)
 | 
			
		||||
    } else if utf8.RuneCountInString(content) > 1 &&
 | 
			
		||||
    (strings.HasPrefix(content, "*") && strings.HasSuffix(content, "*") && !strings.HasPrefix(content, "**") && !strings.HasSuffix(content, "**")) ||
 | 
			
		||||
    (strings.HasPrefix(content, "_") && strings.HasSuffix(content, "_") && !strings.HasPrefix(content, "__") && !strings.HasSuffix(content, "__")) {
 | 
			
		||||
      str := fmt.Sprintf("<%s %s>", options.Name, content[1:len(content)-1])
 | 
			
		||||
 | 
			
		||||
      if options.NoColor {
 | 
			
		||||
        fmt.Print(str + "\n\r")
 | 
			
		||||
      } else {
 | 
			
		||||
        fmt.Print(ansi.Color(str, "green+b") + "\n\r")
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "green+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    } else if options.IsJoin {
 | 
			
		||||
      // TODO
 | 
			
		||||
      channel, err := session.State.Channel(options.Channel)
 | 
			
		||||
      if err != nil {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      guild, err := session.State.Guild(channel.GuildID)
 | 
			
		||||
      if err != nil {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      str := fmt.Sprintf("%s %s has joined %s", timestamp, options.Name, guild.Name)
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "yellow+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    } else if options.IsPin {
 | 
			
		||||
      // TODO
 | 
			
		||||
      str := fmt.Sprintf("%s %s pinned a message to this channel", timestamp, options.Name)
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "yellow+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    } else {
 | 
			
		||||
      nameColor := "cyan+b"
 | 
			
		||||
      if options.IsMention {
 | 
			
		||||
| 
						 | 
				
			
			@ -105,9 +186,8 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) {
 | 
			
		|||
        name = ansi.Color(name, nameColor)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // FIXME: where is this off by 4 actually from
 | 
			
		||||
      padding := strings.Repeat(" ", int(math.Abs(float64(stateNameLength) - float64(nameLength) - 4)))
 | 
			
		||||
      str := fmt.Sprintf("%s%s %s", name, padding, options.Content)
 | 
			
		||||
      padding := strings.Repeat(" ", int(math.Abs(float64(stateNameLength) - float64(nameLength))) + 1)
 | 
			
		||||
      str := name + padding + content
 | 
			
		||||
      if options.IsMention {
 | 
			
		||||
        str = str + "\x07"
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -115,9 +195,27 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: attachments
 | 
			
		||||
  if len(options.Attachments) > 0 {
 | 
			
		||||
    for _, attachment := range options.Attachments {
 | 
			
		||||
      str := fmt.Sprintf("<attachment: %s >", attachment.URL)
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "yellow+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  // TODO: stickers
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if len(options.Stickers) > 0 {
 | 
			
		||||
    for _, sticker := range options.Stickers {
 | 
			
		||||
      str := fmt.Sprintf("<sticker: \"%s\" https://cdn.discordapp.com/stickers/%s.png >", sticker.Name, sticker.ID)
 | 
			
		||||
      if !options.NoColor {
 | 
			
		||||
        str = ansi.Color(str, "yellow+b")
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fmt.Print(str + "\n\r")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: links
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -162,6 +260,7 @@ func ProcessMessage(session *discordgo.Session, msg *discordgo.Message, options
 | 
			
		|||
 | 
			
		||||
  isPing := msg.MentionEveryone || hasMentionedRole || isDirectlyMentioned
 | 
			
		||||
  isDM := channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM
 | 
			
		||||
  isEdit := msg.EditedTimestamp != nil
 | 
			
		||||
 | 
			
		||||
  currentChannel := state.GetCurrentChannel()
 | 
			
		||||
  isCurrentChannel := currentChannel == msg.ChannelID
 | 
			
		||||
| 
						 | 
				
			
			@ -182,6 +281,30 @@ func ProcessMessage(session *discordgo.Session, msg *discordgo.Message, options
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  content, _ := msg.ContentWithMoreMentionsReplaced(session)
 | 
			
		||||
  if isEdit {
 | 
			
		||||
    content = content + " (edited)"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isDump := REGEX_CODEBLOCK.MatchString(content)
 | 
			
		||||
 | 
			
		||||
  if strings.Index(content, "\n") > -1 && !isDump {
 | 
			
		||||
    for _, line := range strings.Split(content, "\n") {
 | 
			
		||||
      options.Content = line
 | 
			
		||||
      options.Name = msg.Author.Username
 | 
			
		||||
      options.Channel = msg.ChannelID
 | 
			
		||||
      options.Bot = msg.Author.Bot
 | 
			
		||||
      options.Attachments = msg.Attachments
 | 
			
		||||
      options.Stickers = msg.StickerItems
 | 
			
		||||
      options.Reply = msg.ReferencedMessage
 | 
			
		||||
      options.IsMention = isPing
 | 
			
		||||
      options.IsDM = isDM
 | 
			
		||||
      options.IsJoin = msg.Type == discordgo.MessageTypeGuildMemberJoin
 | 
			
		||||
      options.IsPin = msg.Type == discordgo.MessageTypeChannelPinnedMessage
 | 
			
		||||
      options.IsDump = false
 | 
			
		||||
 | 
			
		||||
      FormatMessage(session, options)
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    options.Content = content
 | 
			
		||||
    options.Name = msg.Author.Username
 | 
			
		||||
    options.Channel = msg.ChannelID
 | 
			
		||||
| 
						 | 
				
			
			@ -193,9 +316,10 @@ func ProcessMessage(session *discordgo.Session, msg *discordgo.Message, options
 | 
			
		|||
    options.IsDM = isDM
 | 
			
		||||
    options.IsJoin = msg.Type == discordgo.MessageTypeGuildMemberJoin
 | 
			
		||||
    options.IsPin = msg.Type == discordgo.MessageTypeChannelPinnedMessage
 | 
			
		||||
  options.IsDump = REGEX_CODEBLOCK.MatchString(content)
 | 
			
		||||
    options.IsDump = isDump
 | 
			
		||||
 | 
			
		||||
    FormatMessage(session, options)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ProcessQueue(session *discordgo.Session) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								main.go
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								main.go
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -3,6 +3,7 @@ package main
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"atomicgo.dev/keyboard"
 | 
			
		||||
| 
						 | 
				
			
			@ -73,8 +74,22 @@ func main() {
 | 
			
		|||
  // TODO: dont set for user accounts(? never really tested if it matters)
 | 
			
		||||
  client.Identify.Intents = discordgo.IntentsAll
 | 
			
		||||
 | 
			
		||||
  client.AddHandlerOnce(events.Ready)
 | 
			
		||||
  client.AddHandler(events.MessageCreate)
 | 
			
		||||
  if config["useMobile"] == "true" {
 | 
			
		||||
    client.Identify.Properties = discordgo.IdentifyProperties{
 | 
			
		||||
      OS: "Android",
 | 
			
		||||
      Browser: "Discord Android",
 | 
			
		||||
      Device: "Pixel, raven",
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // TODO: user account support
 | 
			
		||||
    client.Identify.Properties = discordgo.IdentifyProperties{
 | 
			
		||||
      OS: runtime.GOOS,
 | 
			
		||||
      Browser: "comcord",
 | 
			
		||||
      Device: "comcord",
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  events.Setup(client)
 | 
			
		||||
 | 
			
		||||
  err = client.Open()
 | 
			
		||||
  if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue